0x01_变量和基本类型
变量和基本类型
变量
变量声明和定义的关系
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:
extern int i; // 声明i而非定义i int j; // 声明并定义j
任何包含了显示初始化的声明即成为定义。在函数体外,能给由extern关键字标记的变量赋一个初始值,但是这么做也抵消了extern的租用。此时语句不再是声明而是定义。
extern double pi = 3.1416; // 定义
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
复合类型
一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成,每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
引用
引用为对象起了另外一个名字,引用类型引用另外一种类型,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:
int ival = 1024; int &refVal = ival; // refVal指向ival,是ival的另一个名字 int &refVal2; // 报错:引用必须被初始化
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定到一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用只能绑定在对象上,而不能与字面值或者某个表达式的计算结果绑定在一起(常量引用除外)。因为引用本身不是一个对象,所以不能定义引用的引用。
指针
指针也实现了对其他对象的间接访问,然而指针与引用相比又有很多不同点。
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。
通过将声明符写成*d的形式来定义指针类型,其中d是变量名:
int *ip1, *ip2; // ip1和ip2都是指向int型对象的指针 double dp, *dp2; // dp2是指向double型对象的指针,dp是double型对象
如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象:
int ival = 42; int *p = &ival; cout << *p;
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上就是给指针所指的对象赋值。
空指针
空指针不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
int *p1 = nullptr; int *p2 = 0; int *p3 = NULL:
把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。
int zero = 0; pi = zero; // 错误:不能把int变量直接赋给指针。
*void 指针:
void *是一种特殊的指针类型,可用于存放任意对象的地址。我们对该地址中到底是个什么类型的对象并不了解。利用void *指针能做的事比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void *指针。
理解复合类型的声明
指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
引用本身不是一个对象,因此不能定义指向引用的指针,但指针是对象,所以存在对指针的引用。
int i = 42; int *p; // p是一个int型指针 int *&r = p; // r是一个对指针p的引用 r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i *r = 0; // 解引用r得到i,即p指向的对象
const限定符
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。初始值可以是任意复杂的表达式。
const int i = get_size(); // 运行时初始化 const int j = 42; // 编译时初始化 const int k; // 错误,k是一个未经初始化的常量
默认状态下,const对象仅在文件内有效。
当以编译时初始化定义一个const对象时,如:
const int bufSize = 512; // 输入缓冲区大小
编译器在编译过程中把用到该变量的地方都替换成对应的值,为了执行上述替换,编译器必须知道变量的初始值,如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
如果const的初始值不是一个常量表达式,且需要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他对象一样工作,即只在一个文件中定义const,而在其他多个文件中声明并使用它。办法是:对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次。
extern const int bufSize = fcn(); extern const int bufSize;
const的引用
可以把引用绑定到const对象上,称之为对常量的引用,对常量的引用不能用作修改它所绑定的对象。
const int ci = 1024; const int &r1 = ci;
常量引用是对const的引用。
一般来说,引用的类型必须与其所引用对象的类型一致,但一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象、字面值,甚至是一般表达式。
int i = 42; const int &r1 = i; // 允许将const int&绑定到一个普通int对象上 const int &r2 = 42; // 正确:r1是一个常量引用 const int &r3 = r1 * 2; // 正确:r3是一个常量引用 int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用
当一个常量引用被绑定到另外一种类型上时:
double dval = 3.14; const int &ri = dval;
为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:
const int temp = dval; // 由双精度浮点数生成一个临时的整型常量 const int &ri = temp; // 让ri绑定这个临时量
对const的引用可能引用一个并非const的对象
必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是一个非常量,所以允许通过其他途径改变它的值。
int i = 42; int &r1 = i; // 引用ri绑定对象i const int &r2 = i; // r2也绑定对象i,但是不允许通过修改r2修改i的值 r1 = 0; // r1并非常量,i的值修改为0 r2 = 0; // 错误:r2是一个常量引用
指针和const
指向常量的指针,不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14; // pi是一个常量,它的值不能改变 double *ptr = π // 错误:ptr是一个普通指针 const double *cptr = π // 正确:cptr可以指向一个双精度常量 *cptr = 42; // 错误:不能给*cptr赋值
允许一个指向常量的指针指向一个非常量对象。
const指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定义为常量。常量指针必须初始化,而且一旦初始化完成,则它的值(即存放在指针中的那个地址)就不能再改变了。不变的是指针本身的值而非指向的那个值:
int errNumb = 0; int *const curErr = &errNumb; // curErr将一直指向errNumb const double pi = 3.14159; const double *const pip = π // pip是一个指向常量对象的常量指针
顶层const
指针本身是不是常量以及指针所指的是不是一个常量是两个相互独立的问题。用名词顶层const表示指针本身是一个常量,用名词底层const表示指针所指的对象是一个常量。
更一般的,顶层const表示任意的对象是常量,底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的,指针类型既可以是顶层const也可以是底层const。
int i = 0; int *const p1 = &i; // 不能改变p1的值,顶层const const int ci = 42; // 不能改变ci的值,顶层const const int *p2 = &ci; // 允许改变p2,底层const const int *const p3 = p2; // 靠右的是顶层const,靠左的是底层const const int &r = ci; // 声明引用的都是底层const
当执行对象的拷贝动作时,常量是顶层const还是底层const区别明显,其中,顶层const不受影响:
i = ci; // ci是顶层const,无影响 p2 = p3; // p3顶层const部分不影响
底层const的限制不能忽视,当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格或者两个对象的数据类型必须能够转换。一般的,非常量可以转换成常量:
int *p = p3; // 错误:p3包含底层const定义,而p没有 p2 = p3; // 正确:p2和p3都是底层const p2 = &i; // 正确:int *能转换成const int * int &r = ci; // 错误:普通的int &不能绑定到int常量上 const int &r2 = i; // 正确:const int &可以绑定到普通int上
constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。显然字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,如:
const int max_files = 20; // 是 const int limit = max_files + 1; // 是 int staff_size = 27; // 不是 const int sz = get_size(); // 不是
在一个复杂系统中,很难分辨一个初始值到底是不是常量表达式。允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,且必须用常量表达式初始化。
constexpr int mf = 20; // 是 constexpr int limit = mf + 1; // 是 constexpr int sz = size(); // 只有当size是一个constexpr函数时,才是一条正确的语句
允许定义一种特殊的constexpr函数,这种函数足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量。
尽管指针和引用都能定义成constexpr,但是它们的初始值却受严格限制,一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。函数体内定义的变量一般来说并非存放在固定地址中,定义于函数所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。
必须明确一点,在constexpr声明中,如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p是一个指向整型常量的指针 constexpr int *q = nullptr; // q是一个指向整数的常量指针
constexpr指针既可以指向常量也可以指向一个非常量:
constexpr int *np = nullptr; // np是一个指向整数的常量指针,其值为空 int j = 0; constexpr int i = 42; // i的类型是整型常量 constexpr const int *p = &i; // p是常量指针,指向整型常量i constexpr int *p1 = &j; // p1是常量指针,指向整数j
处理类型
类型别名
有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
typedef double wages; // wages是double的同义词 typedef wages base, *p; // base是double的同义词,p是double *的同义词
新标准规定了一种新的方法,使用别名声明来定义类型的别名:
using SI = Sales_item; // SI是Sales_item的同义词
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会有意想不到的后果:
typedef char *pstring; // pstring是类型char *的别名 const pstring cstr = 0; // cstr是指向char的常量指针 const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
const是对给定类型的修饰,pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针,声明语句中用到pstring,其基本数据类型是指针。
auto类型说明符
auto让编译器通过初始值来推算变量的类型,auto定义的变量必须有初始值:
auto item = val1 + val2; // item初始化为val1和val2相加的结果
auto能在一条语句中声明多个变量,因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须都一样。
复合类型、常量和auto:
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型:
int i = 0, &r = i; auto a = r; // a是一个整数
auto一般会忽略掉顶层const,同时底层const则会保留下来,必须当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci; auto b = ci; // b是一个整数(ci的顶层const被忽略) auto c = cr; // c是一个整数 auto d = &i; // d是一个整型指针 auto e = &ci; // e是一个指向整数常量的指针
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci; // ci的推演类型是int,f是const int
还可以将引用的类型设为auto:
auto &g = ci; // g是一个整型常量的引用,绑定到ci auto &h = 42; // 错误:不能为非常量引用绑定字面值 const auto &j = 42; // 正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。如果给auto的初始值绑定一个引用,则此时的常量就不是顶层常量了。
ci是初始值,为一个常量,g绑定到ci时,初始值是一个const int型对象,即一个顶层的const对象,此时初始值的顶层常量属性仍然保留,g的类型是const int &;
如果定义一个引用去绑定整型常量ci,此为对常量引用,由于用于声明引用的const都是底层const,用auto去获取此引用类型如变量c。
decltype类型标识符
有时需要从表达式的类型推断出要定义的变量的类型,但不想用该表达式的值初始化变量,使用说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
decltype处理顶层const和引用的方式与auto有不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci = 0, &cj = ci; decltype(ci) x = 0; // x的类型是const int decltype(cj) y = x; // y的类型是const int &,y绑定到变量x decltype(cj) z; // 错误:z是一个引用,必须初始化
引用从来都作为其所指向对象的同义词出现,只有用在decltype处是一个例外。
decltype和引用:
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。有些表达式将向decltype返回一个引用类型。一般来说这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:
int i = 42, *p = &i, &r = i; decltype(r + 0) b; // 正确:加法的结果是int decltype(*p) c; // 错误:c是int &,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,此时表达式的结果是一个具体值而非引用。
另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。
decltype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的dectype就会得到引用类型:
decltype((i)) d; // 错误:d是int &,必须初始化 decltype(i) e; // 正确:e是一个未初始化的int
自定义数据结构
定义Sales_data类型
struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; Sales_data accum, trans, *salesptr;
C++11新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始化值的成员将被默认初始化。
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体(块作用域)之外的变量被初始化为0,定义在函数体(块作用域)内部的内置类型将不被初始化,一个未被初始化的内置类型变量的值是未定义的。
对类内初始值的限制与之前介绍的类似:或者放在花括号里,或者放在等号右边,记住不能使用圆括号。
编写自己的头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量。有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作。
头文件一旦改变,相关地源文件必须重新编译以获取更新过的声明。
使用预处理器可以确保头文件多次包含也能安全工作:
#ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; #endif
预处理变量无视C++语言中关于作用域的规则。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!