跳过正文

丙加·第2章·君の名は。(变量名称、数据类型)

·1489 字·7 分钟·
目录

本章将阐述如何为变量命名,并介绍 C/C++中的基本数据类型。

2.0 名前 は 知 しらないよ~
#

这是泷和三叶的一段对话:

三叶:孤落时辰 已经…快要结束了
泷:三叶,为了醒来的时候不会忘记,把名字写上。来吧
三叶:(写字)ミ——
error: stray ‘\227’ in program
error: stray ‘\129’ in program
……
error: stray ‘\175’ in program
全剧终。(确信)

发生了什么?编译器报错了!它说程序里出现了“迷路的字符”(stray character)。因为早期的 C/C++标准默认只认识 基本的 ASCII 字符(比如英文字母、数字、标点),而三叶名字中的假名 みやみず みつは 属于 Unicode 字符(还有空格!),编译器看到这些它不认识的“乱码”就直接懵了,故事也就无法继续。
虽然 C99/C++11 以后的编译器对 Unicode 字符的支持已经好了很多(部分编译器比如 clang 甚至支持 Unicode 字符作为标识符),但这个故事告诉我们一个重要的道理:在编程的世界里,给事物(变量、函数等)命名,必须遵守一套特定的规则,否则编译器就无法理解你的意图,程序也就无法正常运行。
在编程中,变量(variable)是用来存储数据的容器。每个变量都有两个基本属性:

  • 名称:也就是我们如何称呼它。
  • 类型:决定了容器里能放什么样的数据,以及这个容器的占用空间等等。可以类比为容器分形状和大小。

2.1 变量的命名规则
#

给变量起名不是随心所欲的,必须遵守语言的 语法规则,同时遵循良好的 风格规范

2.1.a 语法规则
#

这是硬性要求,必须遵守:

  • 只能 由字母(a-z, A-Z)、数字(0-9)和下划线(_)组成。
    当然,较新的编译器也支持支持 Unicode 字符作为标识符,比如 int 苹果的数量; 在 clang20 上编译通过。但真心不建议这么做 来回切换输入法真的不累吗?

  • 必须 以字母或下划线开头,不能 以数字开头。

  • 区分大小写age, Age, AGE 是三个不同的变量。

  • 不能 使用 C/C++的 关键字(保留字)作为变量名。例如 int, return, if 等。

    C/C++的关键字列表
    粗体 是两者公用的的,剩下的是 C++特有的,或者较新的 C 标准有的):

    char signed sizeof if extern export friend operator
    wchar_t unsigned typedef else inline class mutable template
    short enum struct do asm private new typename
    int register union while bool public delete typeid
    long volatile switch break true protected this const_cast
    float static case continue false virtual try dynamic_cast
    double const default for using explict throw reinterpret_cast
    void auto goto return nemespace override catch static_cast

    (有很多,还有些省略了)

    这份表格你不必刻意去记,随着学习的深入你会自然熟悉最常用的那些。
    也不用对关键字太过紧张,只要你遵守下面的命名规范,一般都不会出事!

2.1.b 风格规范(软性建议,极力推荐)
#

  • 见名知意:变量名应清晰表达其用途。userAge 远比 uaa 要好。

  • 常用命名法

    • 蛇形命名法 (snake_case): student_name, file_path (常见于 C)

    • 驼峰命名法 (camelCase): studentName, fileName ,也叫小驼峰命名法。(常见于 C++、Java)

    • 帕斯卡命名法 (PascalCase): StudentName, FileName ,也叫大驼峰命名法。(常用于类名、类型名)

    • 全大写+下划线 (UPPER_CASE): 通常用于 常量(const),如 MAX_BUFFER_SIZE

    题外话 PowerShell 的 动-宾命名法: Win + R,输入 powershell 后按下 Enter,就打开了 PowerShell 窗口。在 PowerShell 里面输入 get-command 后按下 Enter,出现的超—级—长——的列表第二列就是命令的名称。比如开头的 Add-AppPackageAdd-AppPackageVolume 还有结尾的 Write-Warning——你会注意到这些都是动词-宾语的结构。
    虽然 C++不能把连字符 - 作为标识符名称,但是我们可以以下划线代替。笔者就偏爱 动-宾+驼峰法 来命名函数。

  • 最小作用域原则:变量名可以尽量短,但前提是它的含义在它所处的小范围(作用域)内是明确且唯一的。在一个只有 5 行的函数里,用 i 做循环计数器是完全可接受的;但在一个全局范围内,变量名就必须足够长和描述性。

    碎碎念
    比如: for (int i = 1 ; i <= 10 ; i++) { 中间的内容不太长... } 是可以接受的,但是 int a(char weuanrcieoihea_palf1145141919810aaaaaaaaa) { 很长的函数体... } 就是找死,不管是函数名称,还是那个脸滚键盘的参数变量名!

2.2 基本数据类型
#

C/C++是一种 静态类型语言,意味着变量在定义时就必须声明其类型,类型一旦确定,就不能更改。这就像给容器贴上了标签,规定它只能装某种东西。

2.2.1 整型 (Integer Types)
#

用于存储整数。区别在于所占内存大小(决定表示范围)和是否能表示负数。

类型 含义 示例 范围 *
int 最常用的整数类型,通常反映机器最“自然”的字长 int a = 5; 有符号:\([-2^{31}, 2^{31}-1]\)
无符号:\([0, 2^{32}-1]\)
short 短整型,范围 一般int short s = 10; 有符号:\([-2^{15}, 2^{15}-1]\)
无符号:\([0, 2^{16}-1]\)
long 长整型,范围 一般 大于等于 intL 是显式后缀,告诉编译器这个变量(至少是)long 这么大 long l = 100000L; 一般和 int 一致
long long 更长的整型,保证至少 64 位(没有 long long long …… 了),LL 是显式后缀 long long ll = 10000000000LL; 有符号:\([-2^{63}, 2^{63}-1]\)
无符号:\([0, 2^{64}-1]\)
unsignedsigned 修饰符,unsigned 表示无符号(仅非负整数);相应的,signed 显式指明有符号(默认如此) unsigned int age = 25; -

* 这是最一般的情况(Windows/MSVC),其他操作系统/编译器实现可能是相似的。这还取决于硬件:比如 16 位单片机(门禁锁、洗衣机里面的微电脑等等)int 就只有 16 位,也就是 2 字节,范围是 \([-32768, 32767]\) ,而你的电脑一般是 32 或者 64 位的,所以范围才是 \([-2^{32}, 2^{32}-1]\) ,也就得到了经典的 \(2147483647\) 。具体范围可以查阅 <limits.h>

你或许会好奇这些范围是怎么计算出来的,以 int 为例,int 在 Windows/MSVC 上占用 2 字节(一个英文字符-> 1 字节),而 1 字节 = 8 位,所以 int 是 32 位长度,32 个 0/1 二进制位可以表示\(2^{32}\)个数字,分别对到 signedunsigned 上就是\([-2^{31}, 2^{31}-1]\)和\([0, 2^{32}-1]\)。

注意:

  1. long 也可以看成修饰符,long int 就是 long,而后面的 long double 则是一种更大、更精确的 double

  2. 具体范围依赖于编译器和操作系统(如 32 位/64 位)。如果需要精确位宽,可使用 <cstdint> (C++11) / <stdint.h> (C99) 中的类型,如 int32_t, uint64_t

附:二进制数据单位转换
在计算机领域,数据单位通常以 1024 (2¹⁰) 为进制,而不是日常生活中的 1000。
不过硬盘、U 盘商家为了方便会按 1000 算。

1 字节(Byte) = 8 位(Bit)
1 千字节(KB) = 1024 字节(Byte)
1 兆字节(MB) = 1024 千字节(KB)
1 吉字节(GB) = 1024 兆字节(MB)
1 太字节(TB) = 1024 吉字节(GB)

2.2.2 浮点型 (Floating-Point Types)
#

用于存储实数(带小数点的数)。

类型 含义 示例 精度说明
float 单精度浮点数 float f = 3.14f; 通常约 6-7 位有效数字
double 双精度浮点数(推荐默认 double d = 3.14159; 通常约 15-16 位有效数字
long double 扩展精度浮点数 long double ld = 3.1415926535L; 精度一般更高,依赖编译器具体实现

浮点数比较时可能存在精度误差,应避免直接用 == 判断相等。
考虑这个例子:

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double a = 0.1;
    double b = 0.2;
    // 设置打印20位小数
    // 然后依次打印 a的值、空格、b的值、空格、a+b的值,最后换行
    std::cout << std::setprecision(20) << a << " " << b << " " << a + b << std::endl;

    return 0;
}

输出:

0.10000000000000000555 0.2000000000000000111 0.30000000000000004441

所以在比较浮点数的时候,不能直接像整数那样用 == 判断相等(变量运算详见下章)。而应该是两变量的差小于某极小值:

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double a = 0.1;
    double b = 0.2;

    std::cout << ((a + b) == 0.3) <<  " " << (abs(a + b - 0.3) < 10e-6)  << std::endl;

    return 0;
}

输出:

0 1     --> false是0 true是1

2.2.3 字符型 (Character Type)
#

用于存储单个字符。

类型 含义 示例 范围
char 字符型,通常占 1 字节,存储 ASCII 字符 char c = 'A'; 有符号:\([-2^{7}, 2^{7}-1]\)
无符号:\([0, 2^{8}-1]\)
wchar_t 宽字符类型,通常占 2 字节,用于支持更大的字符集(如 Unicode) wchar_t wc = L'中'; 有符号:\([-2^{15}, 2^{15}-1]\)
无符号:\([0, 2^{16}-1]\)
  • 字符用 单引号 '' 包裹。宽字符前需加 L 前缀。

  • 本质上存储的是字符对应的编码值(整数),因此 char 本质是一种范围比 int 小的整数。

    char letter = 'A';
    int ascii_value = letter; // ascii_value 的值会是 65
    

碎碎念:关于 wchar_t 和“锟斤拷”

wchar_t 是为了支持像中文、日文这样字符数量庞大的语言而设计的“宽字符”类型。它通常占 2 或 4 个字节,可以表示 Unicode 字符集中的字符。

 #include <cwchar>
 wchar_t chinese_char = L'中'; // 注意前面的L
 std::wcout << chinese_char << std::endl; // 输出需要使用宽字符输出流

那著名的“锟斤拷”(��)乱码是怎么来的呢?这常常发生在字符编码转换过程中。比如,一个用 UTF-8 编码的中文字符(通常占 3 字节)被错误地用单字节的 char 类型(如 ASCII 或 GBK 编码)去解码和显示。系统试图将无效的字节序列解释为字符,就可能产生这些看似毫无意义的汉字组合。所以,未来处理多国语言时,搞清楚编码和用对字符类型至关重要,不然你就成了“锟斤拷”的受害者了!

2.2.4 布尔型 (Boolean Type)
#

该类型的名称是为了纪念英国数学家 George Boole,符号逻辑学的奠基人和贡献者。

bool 用于表示逻辑值:真(true)或假(false)。

  • C 语言 (C99 标准之前):没有内置的布尔类型,通常用 int 表示(0 为假,非 0 为真)。C11 后引入。

    // C语言传统做法
    #include <stdbool.h> // C99后提供了这个头文件来定义bool
    int is_ready = 1; // 1表示真
    
  • C++语言:内置 bool 类型,值为 truefalse

    // C++
    bool is_cpp = true;
    bool is_hard = false;
    

2.2.5 空类型 (Void Type)
#

void 类型表示“无类型”。主要用于:

  1. 指定函数不返回任何值。

    void say_hello() {
       printf("Hello\n");
    }
    
  2. 指定函数不接受任何参数(C 中亦可省略)。

    int get_value(void) { ... }
    
  3. 作为通用指针类型 (void*),后续指针章节详解。

2.3 变量的声明、定义与初始化
#

2.3.1 声明 (Declaration)
#

使用 extern 关键字。告诉编译器变量的名称和类型,但是这个变量定义在别处,所以编译器此时 不分配内存。(你暂时用不到)

extern int external_var; // 声明一个在其他地方定义的变量

2.3.2 定义 (Definition)
#

告诉编译器变量的名称和类型,并 分配内存。一个变量只能被定义一次。

int var_a; // 定义了一个变量(未初始化)
int var_b = 6; // 定义了一个变量(初始化为6)

碎碎念

如果一个变量没有初始化,那么它的值到底是多少,取决于它的“地位”(术语叫“所处的 作用域”)。
考虑下面的例子:

#include <iostream>
// using namespace std;
// 这里注释掉using并使用std::是为了进行演示
// 明确展示了标识符的来源,避免歧义。
// 你当然可以在这种小程序里面直接using

int var_a;

int main() {
    int var_b;
    // 依次打印var_a、空格、var_b,最后换行
    std::cout << var_a << " " << var_b << std::endl;

    return 0;
}

它的输出会是……? 一个 可能(大嘘)的输出:

  0 114514  --> 第一个固定为0,第二个看电脑“心情”

完 全 胜 利?不,是 完全失败!由于 var_a 游离于三界之外(函数、结构、类),是 全局变量(Global Varible),编译器可以,实际上也会将其值“顺手”清零,因为全局变量位于 静态存储区,所有东西都是“预制”好的,程序刚开始运行编译器就可以确定这个变量在内存的哪个位置;所以 var_a 一定是 0。而 var_b 则位于 main 函数体内,是 局部变量(Local Varible),编译器无法“顺手”,这是因为局部变量在 栈存储区,变量可以看成从下往上摞起来,编译器难以确定这个变量在内存的哪个位置,所以 var_b 的值就是上次使用这片内存空间的程序遗留的、无法预测的“垃圾值”。仔细想想,是不是有种 NTR 的感觉?
所以,为了你的程序不染上 NTR,请务必、务必及时赋值。漏掉赋值轻则输出有误,重则直接崩溃!务必引起注意!

2.3.3 初始化 (Initialization)
#

在定义变量的同时赋予其一个初始值。

int count = 0;     // 初始化
char grade = 'A';  // 初始化
double pi {3.14159}; // C++11起的列表初始化(更安全,能防止窄化转换,比如从高精度的double自动转为float)

// 可以但是不推荐,因为容易误认为只有c=1;
int a = b = c = 1;

强烈建议像上面那样在定义变量的同时进行赋值(也就是初始化),以避免使用未初始化的变量导致不可预测的行为。

2.4 变量修饰符
#

说是变量修饰符,其实函数甚至类也可以用,这些修饰符用于改变变量的生命周期、作用域或可变性等特性。它们不仅仅是“变量”修饰符,更准确地说是 存储期说明符类型限定符,可以应用于更广泛的对象(包括函数、类等),现在看不懂的可以跳过,未来学到的时候我会提醒你回来查阅的喵~

2.4.1 const - 常量限定符
#

const 用于定义一个 常量,即其值在初始化后 不可被修改

const double PI = 3.14159;
const int MAX_SIZE = 100;

// 以下代码会导致编译错误:
// PI = 3.14; // error: assignment of read-only variable 'PI'

它可以:

  • 提高代码可读性和安全性
  • 编译器可以进行更好的优化
  • 常用于定义配置参数、数学常量等
  • 在 C++中,全局的 const 变量默认具有内部链接(仅在当前文件可见,在别的文件中无法被另一个说明符 extern 选中,除非声明时也加上 extern

命名约定:常量通常使用全大写+下划线的命名方式,如 MAX_BUFFER_SIZE

在类的成员函数上也可以使用 const,承诺该函数不修改对象的成员变量:

class Cache {
public:
    std::string get_data() const { 
        // 这个const修饰的是成员函数get_data()
        // 表示这个函数不会修改对象的任何成员变量
    }
};

2.4.2 constexpr (C++11) - 常量表达式
#

constexpr 表示该变量或函数在 编译时 就能计算出结果。

// 编译时常量
constexpr int array_size = 100;     
// 编译时可以算出常量的东西
constexpr double calculate_circumference(double r) {
    return 2 * 3.14159 * r;
}

int main() {
    constexpr double radius = 5.0;
    constexpr double circumference = calculate_circumference(radius);
    int arr[array_size]; // 可以用作数组大小
    
    return 0;
}

const 的区别:

  • const 只保证运行时不可修改
  • constexpr 保证在编译时就能确定值,可用于需要编译时常量的场景(如数组大小、模板参数等)
    • 对于 constexpr 函数,就是告诉编译器,“这个函数有可能在编译期求值,能算出来的请都算出来(算不出来就算了等运行期)”
    • C++14 起也支持内部有循环的 constexpr 函数的编译优化了

2.4.3 static - 静态存储期
#

static 的效果根据其使用位置不同而有所差异:

2.4.3.1 局部静态变量
#

void counter() {
    static int count = 0; // 只在第一次调用时初始化
    count++;
    std::cout << "Count: " << count << std::endl;
}

int main() {
    counter(); // 输出: Count: 1
    counter(); // 输出: Count: 2
    counter(); // 输出: Count: 3
    return 0;
}

特点:

  • 默认值为 0
  • 只在第一次执行到定义处时初始化
  • 生命周期贯穿整个程序运行期间
  • 作用域仍局限于函数内部(外部不可访问)

2.4.3.2 全局静态变量/函数
#

static int var1 = 42; // 只在当前文件内可见
int var2 = 42;		  // 也是全局变量,但是外部文件可见

void func() {
    var1 = 100; // 可以访问
}

// 在另一个文件中:
// extern int file_local_var; // 链接错误!无法访问
// extern int var2; 		  // 可以访问

特点:

  • 变量默认值为 0
  • 具有内部链接,仅在定义它的文件内可见
  • 避免命名冲突,实现信息隐藏

2.4.3.3 静态成员
#

(这一部分可以学完类再回来看)

class MathUtils {
public:
    static const double PI; // 静态成员声明
    static int instance_count;

    MathUtils() { instance_count++; }
};

// 静态成员定义(必须在类外定义,除非是constexpr)
const double MathUtils::PI = 3.14159;
int MathUtils::instance_count = 0;

int main() {
    std::cout << MathUtils::PI << std::endl; // 无需创建对象即可访问
    MathUtils obj1, obj2;
    std::cout << MathUtils::instance_count << std::endl; // 输出: 2
    return 0;
}

特点:

  • 默认值为 0(若不赋值)
  • 无需实例化(创建)对象即可访问

2.4.4 extern - 外部链接
#

(学到多文件项目自会用到)

extern 用于声明在其他文件中定义的变量或函数 Boy♂Next♂Door ,这在多文件项目中很有用。

file1.cpp:

int global_var = 100; // 定义
extern const int MAX_SIZE = 200; // 常量需要显式指定extern才能外部链接

void utility_function() {
    // 函数实现
}

file2.cpp:

extern int global_var; // 声明,使用file1.cpp中定义的变量
extern const int MAX_SIZE; // 声明外部常量
extern void utility_function(); // 声明外部函数

int main() {
    global_var = 200; // 修改的是file1.cpp中的变量
    utility_function();
    return 0;
}

2.4.5 杂七杂八
#

剩下还有些暂时用不到,放在下面按需查阅

说明符 效果 示例
volatile 告诉编译器该变量可能被程序外部(比如其他程序)修改,阻止编译器优化,确保每次访问都从内存读取 volatile bool data_ready = false;
while (!data_ready) { } // 每次都会检查内存
mutable 允许类的成员变量在 const 成员函数中被修改,打破 const 限制
右边的 const 函数虽然承诺不修改成员,但是 mutable 解除了限制
class Cache {
mutable bool cache_valid;
string get_data() const {
cache_valid = true; // OK
}
};

2.5 类型推导(C++11 起)
#

现代 C++提供了自动类型推导机制,让编译器根据初始化表达式推断变量类型。

又一个轮椅(语法糖)

2.5.1 auto 关键字
#

(这个后面会用到,所以现在不用看)

auto i = 42;           // i 被推导为 int
auto d = 3.14;         // d 被推导为 double
auto s = "hello";      // s 被推导为 const char*
auto v = std::vector<int>{1, 2, 3}; // v 被推导为 std::vector<int>

// 在循环中特别有用
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
for (auto& name : names) { // auto& 避免拷贝
    std::cout << name << std::endl;
}

2.5.2 decltype 关键字
#

decltype 返回表达式的类型。

double x = 3.1415;
// y 的类型与 x 相同,即 double
// 这样可以保证类型统一
// 只需要修改一次 x 的类型其他的都会变
decltype(x) y = 20; 

std::vector<int> vec;
// size 的类型是 std::vector<int>::size_type
// 这里可以使用 auto
decltype(vec.size()) size = vec.size(); 

// 与auto结合使用 (C++14)
decltype(auto) z = x; // z 的类型推导规则与decltype(x)相同

2.6 类型别名
#

为了提高代码可读性和维护性,可以为复杂类型创建别名。

2.6.1 typedef(传统 香烟 方式)
#

typedef unsigned int uint;
typedef std::vector<std::pair<std::string, int>> NameAgeList;

uint age = 25;
NameAgeList people = {{"Alice", 30}, {"Bob", 25}};

2.6.2 using (C++11 新方式)
#

都什么年代了还在抽传统香烟 不是瑞克我不抽

玩梗归玩梗,吸烟有害健康
using uint = unsigned int;
using NameAgeList = std::vector<std::pair<std::string, int>>;

// 模板别名(typedef无法做到)
template<typename T>
using StringMap = std::map<std::string, T>;

StringMap<int> age_map; // 等价于 std::map<std::string, int>

检验下你的学习成果吧,完成下列习题:

  1. 找出以下变量名中的错误项,并说明原因:
    2things, my var, double, $price, _sys_flag

  2. 选择合适的数据类型定义变量来表示以下数据:

    • 一个人的年龄

    • 圆周率 π

    • 考试是否及格

    • 地球的人口数量

    • 一个英文字母

    • (*)银行系统的存款

  3. int a;int a = 0; 有什么区别?分作用域考虑。

命令提示符@CommandPrompt-Wang
作者
命令提示符@CommandPrompt-Wang