C++基本介绍
# 基本内置类型
C++中定义了一套包括算术类型和空类型在内的基本数据类型。其中算术类型包含了字符、整数型、布尔值、浮点数。空类型则不对应任何具体的值。
# 算术类型
C++的算术类型和C一样,也分为两类:整数(包括字符和布尔类型)和浮点数。 C++中的算术类型如下:
类型 | 含义 | 32位占用 | 64位占用 |
---|---|---|---|
void | 空类型 | 0位 | 0位 |
bool | 布尔类型 | 1位 | 1位 |
char / unsigned char | 字符 | 1字节 | 1字节 |
short / unsigned short | 短整数 | 2字节 | 2字节 |
int / unsigned int | 整数 | 4字节 | 4字节 |
long / unsigned long | 长整数 | 4字节 | 8字节 |
long long / unsigned long long | 长长整数 | 8字节 | 8字节 |
float | 单精度浮点数 | 4字节 | 4字节 |
double | 双精度浮点数 | 8字节 | 8字节 |
long double | 扩展精度浮点数 | 有效10字节/占用12字节 | 有效10字节/占用16字节 |
# 自动类型转换
在C++中,如果我们给某种类型的对象强行赋了一个其他类型的值,C++会对该值自动进行类型转换。以下是一些会触发自动类型转换的情况:
- 将非布尔类型的算术值赋值给布尔类型对象时,如果初始值为0则结果为false,否则结果为真。
bool i = 10; // 结果为true
- 将布尔值赋值给非布尔类型对象时,如果初始值为false则结果为0,初始值为true则结果为1。
int i = true; // 结果为1
- 将浮点数赋值给整数类型对象时,结果值仅保留小数点前面的整数部分。
int i = 3.14; // 结果为3
- 将整数赋值给浮点类型对象时,会转化为double类型,小数部分记0,如果整数超过浮点类型容量,精度可能丢失。
double i = 3; // 结果3.0
- 赋值一个超出无符号类型范围的值时,结果为有初始值对无符号类型所能表示的总数取模后的余数。
unsigned char i = -1; // 结果为-1 % 256 = 255 需要注意:运算时切勿混用有符号类型和无符号类型。因为这两者进行运算时,有符号值也会像这样转换为无符号值。
- 赋值一个超出有符号类型范围的值时,结果是未定义,程序可能崩溃或生成垃圾数据。
char i = 256;
# 字面值常量
每个字面值常量都对应一种数据类型,例如:1是整数字面量,1.0是浮点数字面量。
整数字面指针量
我们可以将整数字面量写作十进制、八进制或十六进制的形式:
- 默认为十进制:20
- 以0开头代表八进制:024
- 以0x或0X开头代表十六进制:0x14
虽然整数字面量可以写作八进制或十六进制或十进制三种形式,但实际这三种形式对于计算机来说都是完全一样的,因为计算机只以二进制进行存储。
字符与字符串字面量
由单引号括起来的字符称为字符类型字面值,双引号括起来一个或多个字符称为字符串类型字面值。C++的字符串和C一样也是由常量字符构成的数组,且同样以'\0'空字符结尾。
'a' // 字符字面值
"Hello World!" // 字符串字面值
布尔和指针字面量
布尔类型的字面量有true和false分别表示真假,注意全小写。
指针类型的字面量有nullptr表示空指针。
# 常量表达式
常量表达式是指值不会改变,并且在编译过程中计算结果就已经确定的表达式。
常量表达式能够使程序执行更有效率。因为常量表达式是编译时计算,只需要编译时计算一次。而非常量表达式是运行时计算,在程序启动或每次函数被调用时都要进行计算。
字面值属于常量表达式,使用常量表达式初始化的const对象也是常量表达式,例如:
// 使用字面值,所以是常量表达式
const int max_files = 20;
// 使用常量进行运算,所以是常量表达式
const int limit = max_files + 1;
// 虽然使用字面值,但不是const对象,所以不是常量表达式
int staff_size = 27;
// 虽然是const对象,但具体值在调用时才能获取到,所以不是常量表达式
const int sz = get_size();
// 虽然是const对象,但它依赖一个非常量的表达式staff_size,所以不是常量表达式
const int size_limit = staff_size + 1;
2
3
4
5
6
7
8
9
10
11
在一个复杂的系统中,很难通过初始值区去分辨是不是常量表达式。我们可以使用constexpr关键字来定义一个常量表达式类型的对象。
编译器在编译时会验证constexpr类型对象的值是否是一个常量表达式。以确保声明为constexpr类型的对象一定是一个常量,且初始化为常量表达式。
例如:
// 是常量表达式
constexpr int mf = 20;
// 是常量表达式
constexpr int limit = mf + 1;
// 如果size为constexpr函数时,则不会报错,否则该语句会报错
constexpr int sz = size();
int a = 10;
// 在常量表达式中引用非常量a,所以该语句会报错
constexpr int limit = a + 1;
2
3
4
5
6
7
8
9
10
11
新标准允许定义一种特殊的constexpr函数,这种函数需要简单到在编译时就能计算其结果。只有这种特殊的函数能在constexpr类型对象初始化中被调用。
# 变量
变量提供一个具名的、可供程序操作的存储空间。C++中每个变量都有其对应的数据类型,另外C++中认为万事万物皆可为对象,其中变量也可以称为对象。
# 定义变量
C++的变量同C一样,先是类型说明符(可以是内置类型/库类型/自定义类型),然后紧跟一个或多个变量名组成的列表,多个变量名以逗号分隔,最后以分号结束。
// 内置类型,表示声明三个int类型,其中value为未定义
int sum = 10, value, units_sold = 10;
// 库类型,表示声明一个名为content的可变长字符序列变量,content可作为字符串变量使用。
std::string content = "Hello World!";
// 自定义类型,表示声明一个名为item的Sales_item类型变量。
Sales_item item;
2
3
4
5
6
7
8
当在一条定义语句中定义多个变量时,会按从左往右的顺序进行创建,如果前一个变量在定义时就初始化了值,那么后面的变量定义就可以在初始化中引用它。
例如:double price = 99.98, total = price * 12.5;
# 初始化
初始化不是赋值。初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。
# 拷贝初始化
C++中使用等号进行初始化的方式被称为复制初始化,会将等号右侧的字面量赋值给左侧的变量。
# 直接初始化
C++中使用括号进行初始化的方式被称为直接初始化。
# 列表初始化
C++中使用花括号进行初始化的方式被称为列表初始化。 定义内置类型变量时如果使用列表初始化,且初始值存在丢失信息的风险,则编译器会报错。例如在自动类型转化可能会丢失部分值的情况。
例如:
// 以下4种方式都可以将test的值初始化为0
// 拷贝初始化
int test = 0;
// 列表初始化
int test = {0};
// 列表初始化
int test{0};
// 直接初始化
int test(0);
// 以下情况编译器会报错,因为使用列表初始化,且存在丢失部分值的风险
int a{3.14};
2
3
4
5
6
7
8
9
10
11
12
# 默认初始化
如果在定义变量时没有指定初始值,则变量会被默认初始化,此时变量将被赋予默认值,不同的数据类型其默认值也会不同,另外定义变量的位置也会对此有影响。
如果未被显示初始化的变量是内置类型,那么它的值将由定义的位置决定。如果变量被定义于任何函数体之外,变量会被默认初始化为0;如果变量被定义于任何函数体之内,变量将不被初始化其值为未定义的,任何形式的拷贝或访问都将引发错误。
大多数类都提供默认值,例如std::string类型的默认值为""空字符串。
# 声明与定义
在C++中声明和定义是两个概念。声明用于向程序表明名称的类型和名字,不会分配内存空间。而定义用于为名称分配内存空间,还可以指定初始值。一个名称只能被定义一次,但可以被多次声明。大多数情况下定义包含了声明,但是声明不包含定义。 想要声明一个变量,只需要在变量声明或函数声明前面加extern关键字即可。表示该变量或函数是在别的文件中定义的,要在此文件中引用,提示编译器要从别的文件中寻找。
在声明全局变量时,不同文件在编译时是不透明的,比如在A文件中定义int i,同时在B文件中定义int i,编译器编译时不会报错,但在链接时会报重复定义。 所以当需要使用统一全局变量时,就需要使用extern关键字表示该变量定义在别的文件中,编译器在链接的时候会自动去其他文件查找声明的变量。 例如:在A文件中定义了int i,在B文件中需要调用i,只需要在B文件中声明extern int i。
// 声明,不会分配内存空间
extern int i;
// 声明并定义,会分配内存空间。
int i;
// 声明并定义,会分配内存空间,但这样extern关键字将毫无意义,所以不会这样使用。
extern int i = 10;
2
3
4
5
6
注意:如果在函数体内部初始化一个由extern关键字标记的变量,会引发报错。
# 常量
常量的值不会随着程序的运行而变化。在C++中,我们可以使用const限定符来声明常量,const关键字能够让我们对变量加以限定,禁止我们对其修改。 在定义const对象时,我们必须对其进行初始化,否则会引发报错。 例如:
const int i = 10;
默认情况下,const对象仅在当前文件内有效,如果我们需要引用其他文件的const对象,则需要使用extern关键字对其进行声明。 例如:
extern const int i;
# 输出变量类型
我们可以通过"typeid(变量名).name()"方法来获取变量的类型名称,例如:
double a = 3.14;
// 输出"double"
std::cout << typeid(a).name() << std::endl;
2
3
# 名称
# 名称标识符
C++的标识符由字符、数字、下划线组成,它没有长度限制、对大小写敏感、不能以数字开头。 用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头,另外定义在函数体外的标识符不能以下划线开头。 约定俗成的命名规范:
- 标识符要体现实际含义。
- 变量名一般全小写字母。
- 自定义的类名一般以大写字母开头。
- 如果标识符由多个单词组成,则使用下划线法或驼峰法。
# 名称作用域
作用域是程序的一部分,大多情况下以花括号分隔。同一个名称在不同的作用域中可能指向不同的实体,名称的有效区域始于名称的声明语句,以声明语句所在的作用域末端结束。 最外层的作用域被称为全局作用域,其中的名字在整个程序范围内都可以使用,但其有效区域仍然始于名称的声明语句。花括号内的作用域被称为块作用域,其中的名字在整个块范围内都可以使用。
# 嵌套作用域
作用域能够彼此嵌套,被嵌套的作用域被称为内层作用域,用于嵌套的作用域被称为外层作用域。 内层作用域可以访问外层作用域的名称,也可以定义外层作用域已有的名称(但不会作用于外层作用域的名称)。 例如:
#include <iostream>
// a拥有全局作用域
int a = 1;
int main() {
// 嵌套作用域
{
// 输出1,此时引用的是全局作用域的a变量
std::cout << a << std::endl;
// 该定义会覆盖全局名称
int a = 2;
// 输出2,此时引用的是嵌套作用域的a变量
std::cout << a << std::endl;
// 输出1,使用::符号并且左侧留空,表示在全局作用域查找a变量。
// 同样如果要修改全局变量a的值,也需要使用::a来表示,例如::a = 1
std::cout << ::a << std::endl;
}
// 输出1,在嵌套作用域之外所以覆盖定义的a在这里不起作用,会使用全局作用域的a变量
std::cout << a << std::endl;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
# 复合类型
# 介绍
复合类型是指基于其它类型定义的类型,例如引用和指针就是复合类型。 复合类型的声明由一个基本数据类型和紧随其后的声明符组成,每个声明符后命名一个变量。 复合类型的声明符号可以挨着数据类型,也可以挨着变量名,也可以左右都用空格隔开,这几种都不影响定义。例如:
// 下面这几种方式都是完全一样的
int * a;
int * a;
int* a;
int *a;
// 需要注意,这里a是指针类型,b是int类型,同等于int *a, b;
int* a, b;
2
3
4
5
6
7
8
# 引用类型
# 介绍
引用又称左值引用,可以用于为对象另起名字,引用类型是引用另一类型的复合类型,在定义时需要指定引用对象的类型。 定义时需要通过将声明符写成"&变量名"的形式来定义引用类型,另外引用必须被初始化,且初始化的值只能是对象,而不能是字面量。 例如:
// 定义一个int类型变量。
int ival = 1024;
// 定义一个引用int类型的变量,来引用ival对象的值内存空间。
// ival对象的值会被绑定给ref,等于多了一个入口能访问分配给ival对象的内存空间。
int &ref = ival;
// 会报错,引用必须被初始化。
int &ref2;
// 这里因为ref引用了ival对象的值内存空间,所以此处ref3引用的仍是ival对象的值内存空间。
int &ref3 = ref;
// 可以在一条语句中混用赋值或引用来定义多个名称。
int &reff = ref, nval = ival; // 引用、定义int
int &refs = reff, &refa = nval; // 引用、引用
int one = 1, &refone = one; // 定义int、引用
// 需要注意,引用类型一旦定义,就已经绑定了。后面使用名称进行赋值,都只是修改,而不是引用新对象
int a = 1, b = 2;
int& c = a;
// 输出1
std::cout << c << std::endl;
// 只会将b的值2赋值给c和a,而不是c变成引用b
c = b;
// 输出2
std::cout << c << std::endl;
b = 3;
// 仍然输出2,因为c不是引用b
std::cout << c << std::endl;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
引用本身不是一个对象,所以无法定义引用的引用。
# 常量引用
常量引用即使用了const限定符的引用类型。引用特性可以加快数据的传递效率,常量特性可以防止意外导致修改数据,提高安全性。
在C++中我们还可使用常量引用去引用非常量的对象、字面量、表达式。另外常量引用只保证通过该引用不能修改数据,其他途径还是能对该对象进行修改。
- 当一个非常量对象被一个常量引用绑定时,常量引用会为该非常量对象创建了一个不可变的视图,当原对象的值改变时,常量引用观察到的值也会跟着改变。
- 当一个临时量对象(字面量、表达式、函数返回值)被一个常量引用绑定时,该临时对象的生命周期会被延长,持续到常量引用本身的生命周期结束。
临时量对象是在表达式求值过程中临时创建并用来暂存求值结果的未命名对象。临时对象通常在表达式完全求值后就会被销毁,它们的生命周期非常短暂。
另外如果引用对象是常量,则引用也必须指定为常量,即使用常量引用。
例如:
int a = 10;
// 定义一个常量引用
const int &b = a;
// 由于a不是常量,所以可以修改
a = 20;
// 由于b是常量引用,所以会报错
b = 20;
// 常量引用还可以是字面量
const int &c = 10;
// 常量引用还可以是表达式
const int &d = a * 5;
std::cout << d << std::endl;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 指针类型
# 指针介绍
指针类型是"指向"另一类型的复合类型,用于存放某个对象的地址,与引用类似,指针也可以实现对其他对象的间接访问,在定义时也需要指定目标对象的类型,以保证能够按正确的长度读取目标对象的值内存空间。 区别在于,指针本身就是一个对象,允许对指针进行赋值和拷贝。在指针的生命周期内还可以先后指向几个不同的对象,不像引用一旦初始化就不可更改对象。指针在定义时还可以不赋初始值,和其他内置类型一样,块中定义的指针如果没有被初始化,值也将是未定义的。 定义时需要通过将声明符写成"*变量名"的形式来定义指针类型,如果在一条语句中定义了多个指针变量,则每个指针变量前面都必须有*符号。 例如:
int *ip1, *ip2;
double *dp1, *dp2;
2
# 取地址和解引用
指针用于存放某个对象的地址,要想获取对象的值内存空间的地址,需要使用"&"取地址符来获取,其格式为"&变量名"。 要想通过指针对象来访问其所指向的对象,则需要使用"*"解引用符来访问,其格式为"*指针变量名" 例如:
int i = 10, * iptr = &i;
// 指针本身也是对象,也能通过&获取其内存地址
std::cout << "指针的内存地址为:" << &iptr << std::endl;
// 指针存储的值本身就是对象的内存地址
std::cout << "所指向对象的内存地址为:" << iptr << std::endl;
// 通过*解引用获取存储对象的值,结果为10
std::cout << "所指向对象的值为:" << *iptr << std::endl;
// 通过*解引用修改存储对象的值
*iptr = 20;
// 通过*解引用获取存储对象的值,结果为20
std::cout << "所指向对象的值为:" << *iptr << std::endl;
2
3
4
5
6
7
8
9
10
11
注意:解引用操作仅适用于那些确实指向了某个对象的有效指针,对于空指针或无效指针则不适用。
# 空指针
空指针(nullptr)不指向任何对象。在对一个指针进行操作之前,我们可以先检查它是否为空。例如:
// 建议使用该方式更直观,nullptr的值实际就是0,等价于int *p1 = 0
int *p1 = nullptr;
// 赋值0代表空指针,因为空指针的内存地址全为0
int *p2 = 0;
// 预处理变量NULL的值实际就是0,但是使用它需要先导入cstdlib库
int *p3 = NULL;
// 该方式是错误的,不能将变量直接赋值给指针,即便其值为0
int a = 0, *iptr = a;
2
3
4
5
6
7
8
9
# void*指针
void*是一个特殊的指针类型,可用于存放任意类型对象的地址。 void*指针能做的事比较有限。我们可以将void*指针作为函数的输入或输出。但我们无法对void*指针所指向的对象进行操作,因为程序不知道指向的对象到底是什么类型,也就无法确定能在该对象上做哪些操作。 例如:
int a = 10;
void *aptr = &a;
2
# 指向指针的指针
在声明指针时的修饰符并没有个数限制,且指针本身也是内存中的对象,因此我们在声明时可以用一个指针指向另一个指针。 通过*的个数可以区分指针的级别,例如**表示指向指针的指针,***表示指向指针的指针的指针,以此类推。 例如:
int ival = 1024;
// pi指向ival
int *pi = &ival;
// ppi指向pi
int **ppi = π
// pppi指向ppi
int ***pppi = &ppi;
// 在解引用时,也会根据*的个数去解引用
std::cout << *pi << std::endl;
std::cout << **ppi << std::endl;
// 此处只解一层的话,其值就是ppi存储的值
std::cout << *pppi << ppi << std::endl;
// 此处解两层的话,其值就是pi存储的值
std::cout << **pppi << pi << std::endl;
// 此处解三层的话,其值就是ival存储的值
std::cout << ***pppi << ival << std::endl;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 指向指针的引用
引用本身不是对象,因此不能定义指向引用的指针,但是指针是对象,所以可以定义引用指针的引用。 引用指针时,修饰符需要*符号在前,&符号在后,因为&对变量有最直接的影响。
面对一条比较复杂的指针或引用的声明语句时,从右往左阅读有助于弄清楚它的真实含义。离变量名越近的符号对变量的类型有最直接的影响。 例如:
int i = 1024;
int *p;
// 引用指针
int *&r = p;
// 同等于p = &i
r = &i;
// 同等于*p=10或i=10
*r = 10;
2
3
4
5
6
7
8
# 指向常量的指针
指向常量的指针又称为底层const,表示指针所指的对象是一个常量。
与引用一样,可以令指针指向常量或非常量。如果指向对象被定义为常量,则必须使用指向常量的指针。
使用底层const指针后,我们就无法通过该指针修改所指向对象的值了,但是我们仍然可以修改指针自身的值。
使用底层const方式指向非常量,也是一种底层const指针。
例如:
// 指向常量时,必须使用底层const指针
const int i = 10;
const int *iptr = &i;
// 也可以指向非常量
int a = 10;
const int *aptr = &a;
// 会报错,不能通过底层const指针修改对象的值
*aptr = 1;
// 但是可以通过其他途径修改所指向对象的值
a = 20;
// 也可以修改指针自身的值。
int b = 20;
aptr = &b;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# const指针
const指针又称为顶层const,表示指针本身是一个常量。
const指针即常量指针,指针是对象,因此可以像其他类型一样将指针本身定义为常量。常量指针在定义时必须初始化。
使用常量指针后,常量指针的值(所指向对象的内存地址)就不能再改变了,但可以通过常量指针修改其所指向对象的值。
定义常量指针的格式是:数据类型 *const 指针名 = &变量名;
例如:
int i = 0;
int a = 10;
// 定义一个常量指针指向i变量
int *const iptr = &i;
// 会报错,不能修改常量指针的值。
iptr = &a;
// 但是可以修改指针所指向的对象的值。
*iptr = 20;
2
3
4
5
6
7
8
另外指针类型既可以是顶层const又可以是底层const。也就是指针本身是常量,其所指向的对象也是常量。此时即不允许修改指针本身的值,也不允许修改指针所指向对象的值。例如:
int i = 0;
int a = 10;
// 定义一个即是顶层const又是底层const的指针
const int *const iptr = &i;
// 通过该指针进行任何修改都是不允许的
*iptr = 10; // 不允许,会报错
iptr = &a; // 不允许,会报错
2
3
4
5
6
7
8
# constexpr指针
constexpr限定符也可以用于定义指针,但它的初始值必须为nullptr或0,或者是存储于某个固定地址的对象。
因为函数体内定义的变量的内存地址一般都不是固定的,因此constexpr指针不能指向这样非固定的地址。
但相对的,定义于所有函数体之外的对象,其内存地址是不变的,此类地址就能用来初始化constexpr指针。
constexpr限定符仅对指针本身有效,与指针所值的对象无关。constexpr会将它所定义的对象置为顶层const。
例如:
int i = 1;
int main(){
// 定义一个constexpr指针指向空指针
constexpr int *p = nullptr;
// 定义一个constexpr指针指向全局作用域的i变量
constexpr int *ip = &i;
}
2
3
4
5
6
7
8
# 处理类型
# 类型别名
类型别名用于给类型取别名,使用类型别名可以让复杂的类型名称变得简单明了,易于理解和使用。
定义类型别名可以使用typedef关键字,其格式为typedef 类型 类型别名;
。或使用新方法using来做别名声明,其格式为using 类型别名 = 类型;
。
例如:
- typedef
// 给double类型创建别名dob和doub
typedef double dob, doub;
// 给double*创建一个dobp别名,也就是创建一个指针类型的别名
typedef dob *dobp; // 同等与typedef double* dobp;
// 使用别名定义变量
dob d = 1.5;
dobp a = &d;
std::cout << *a << std::endl;
2
3
4
5
6
7
8
9
- using
using dob = double;
using dobp = double*;
dob d = 1.5;
dobp a = &d;
std::cout << *a << std::endl;
2
3
4
5
# auto类型说明符
auto类型可以让编译器自动分析初始值表达式所属的类型,并用分析出来的类型作为对象的类型,因此auto定义的变量必须有初始值。另外auto类型也能分析自定义类型。 auto类型定义指针时,*号可以忽略。但在定义引用时,需要指定&符号。
实现类似于Python、JS一样的动态类型语言的功能,不过只能在定义时使用。 例如:
// 此处i为int类型
auto i = 1 + 1;
// 此处i为double类型,表达式会进行自动类型转换
auto i = 1 + 1.0;
// 此处i为int类型,ip为整数指针类型,因为&i是一个内存地址
auto i = 0, ip = &i;
// 此处i为int类型,r为i的引用
auto i = 0, &r = i;
// 此处会报错,在一条语句声明多个变量时,其初始值类型必须全部一样
auto i = 1, d = 3.14;
2
3
4
5
6
7
8
9
10
11
auto类型也可以使用const关键字,用于定义常量引用或顶层const指针。 例如:
// 顶层const,ip指针本身的值无法进行修改
int i = 10;
const auto ip = &i;
// 底层const,ip指针所指对象的值无法进行修改
const int i = 10;
auto ip = &i;
// 即是顶层const又是底层const
const int i = 10;
const auto ip = &i;
// 常量引用,无法通过常量引用修改所引用对象的值
int i = 10;
const auto &a = i;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# decltype类型指示符
decltype用于推导变量、表达式或函数调用的数据类型,并让我们能够使用推导的类型定义变量。
它能在编译时分析表达式的类型信息,但不会实际执行表达式,包括函数也是只会分析不会调用执行。另外如果表达式的内容是解引用操作,则decltype将得到引用类型。
decltype能让我们编写出更加通用和灵活的代码,它的使用形式是:decltype(表达式)
例如:
// 案例一
// 此处decltype(i)查询到i的类型并返回,所以同等与int a = i。
int i = 10;
decltype(i) a = i;
// 案例二
// 此处decltype(foo())分析结果为该函数的返回类型。这种方式能让我们无需事先知道函数返回类型,即可接收函数的返回值。
decltype(foo()) f;
// 此处调用foo()并将返回值赋值给f。
f = foo();
// 案例三
int i = 10, *ip = &i;
// 此处decltype(ip)分析为整数指针类型,因此iptr会是整数指针类型。
decltype(ip) iptr;
// 此处decltype(*ip)会对ip解引用得到&i,因此ir会是整数引用类型,必须进行初始化。
decltype(*ip) ir = i;
// 案例四
// 此处decltype(ip)分析为引用,因此rt也会引用类型,所以必须初始化,否则会报错。
int i = 10, &r = i;
decltype(r) rt = r;
// 案例五
// 此处decltype(r + 10)中,虽然r是引用,但表达式结果是一个具体的值,因此a会是double类型。
int i = 10, &r = i;
decltype(r + 0.5) a;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 自定义头文件
# 预处理器
预处理器是在编译之前执行的一段程序,可以部分改变我们缩写的程序。例如预处理器看到#include标记时,就会使用#include所包含的头文件的文本内容,来对#include标记进行替换。
# 编写头文件
为了保证类的定义在所有文件中都一致,我们一般会将类定义在自定义的头文件中,在需要使用时进行包含。头文件名称要尽量符合实际作用,如果仅用来包含类,则可以和类名一致。 头文件通常包含只需要被定义一次的实体,如类、const和constexpr变量、函数等,也会包含一些被该头文件所用到的其他头文件。另外我们对头文件进行修改时,用到它的源文件必须重新编译以进行更新。
我们在自定义头文件时,需要通过预处理器来实现一项头文件保护符功能,来确保同一头文件只会被包含一次。 头文件保护符依赖于预处理变量,预处理变量就是用#define定义的变量。我们可以用#ifndef指令来判断某个预处理变量是否已定义,如果变量未定义则为真,会执行后续操作,直到遇到#endif指令为止。在执行操作时我们要定义那个用于判断的预处理变量,来确保后续相同#include标记不会被执行。
另外还有#ifdef指令,已定义则为真。 另外还可以使用更为简洁的"#pragma once"预处理指令,作用都是保证头文件只会被编译一次,只要在头文件的最开始加入这条预处理指令即可。大部分编译器都支持该预处理命令。 以下是一个头文件代码例子:
// 第一种方式(所有编译器都支持):
#ifdef BOOK_DATA_H
#define BOOK_DATA_H
#include <iostream>
struct Book {
std::string name = "Hello World!";
int price;
struct {
const std::string name = "page";
int num = 0;
} page;
};
#endif
// 第二种方式(大部分编译器都支持):
#pragma once
#include <iostream>
struct Book {
std::string name = "Hello World!";
int price;
struct {
const std::string name = "page";
int num = 0;
} page;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26