从C到C++,我要面朝对象了。
从 C 到 C++
cout
中的 c 指的是 console。endl
=> end line。作用域限定符
::
相当于中文的
,表示作用域或所属关系。extern "C"
有时候在 C++ 工程中可能需要将某些函数按照 C 的风格来编译,在代码前加 extern “C” ,意思是告诉编译器,将该函数按照C语言规则来编译。
1
2
3
4extern “C”
{
something();
}typeid(变量名).name
输出变量的类型。bool
类型,true 为真,false 为假。任何基本类型都可以隐士转换为 bool 类型,非 0 即真,0 即假。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/*
boolalpha -> 相当于一个开关,表示开,打印 true / false
noboolalpha -> 表示关,关闭后打印 0 / 1
*/
bool a = false;
cout << a << " " << boolalpha << a << endl;
a = 20;
cout << noboolalpha << a << endl;
a = *("abc"+3); // 0->a 1->b 2->c 3->\0
cout << boolalpha << a << endl;
/*
这三种意思相同
a = *("abc"+3);
a = "abc"[3];
a = 3["abc"];
*/
命名空间(namespace)
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。
相同名字的 namespace 作用域相同,同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
命名空间的作用:避免名字冲突、划分逻辑单元,名字空间中的声明和定义可以分开。
名字空间可以嵌套,使用时候得一层一层的扒皮。
名字空间也可以赋值取别名,🌰 栗子:
1
2
3
4
5
6
7
8
9
10
11
12namespace A1
{
namespace A11
{
namespace A12
{
something();
}
}
}
namespace A = A1::A11::A12;
// A1::A11::A12::something(); 等价于 A::something();using namespace A1;
就相当于”裸奔“ ,把 A1 中的东西暴露在当前作用域下。using namespace std;
也是一样,把 cout 暴露在全局下。风险:可能会出现命名冲突,一般还是带上
::
。
函数
缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
全缺省参数
1 | void TestFunc(int a = 10, int b = 20, int c = 30); |
半缺省参数
1 | void TestFunc(int a, int b = 10, int c = 20); |
缺省参数必须从右开始设置。
1
/*ERROR*/ void fun(int a = 3,char b,char *c = "ahoj");
缺省参数不能在函数声明和定义中同时出现,建议声明时指定。如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
缺省值必须是常量或者全局变量。
C语言不支持(编译器不支持)。
哑元
只指定类型而不指定名称的函数,占着茅坑不拉屎。
🌰 栗子:
1 | void ya(int,int b) |
- 兼容老版本。
- 支持函数重载。
重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了”,后者是“谁也赢不了” 。函数重载:是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 / 类型 / 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
🌰 栗子:
1 | void foo(){ |
- 同一作用域,函数名相同,参数表不同的函数。
- 参数表不同:
- 参数类型不同
- 参数个数不同
- 参数顺序不同
- 重载和形参名无关。
- 重载和返回类型无关。
- 不同作用域同名函数遵循就近原则。
重载的原理
nm a.out
,查看 C++ 编译器给看书取得名字:
1 | 00000001000010a0 T __Z3Maxii |
C++ 函数重载通过编译器改名实现。
名字修饰(Name Mangling)
在 C/C++ 中,一个程序要运行起来,需要经历:预处理、编译、汇编、链接。
Name Mangling 是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。
C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。
C++ 要支持函数重载、命名空间等,使得其修饰规则比较复杂,不同编译器在底层的实现方式可能都有差
异。
被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数
列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终
的名字中,就可保证名字在底层的全局唯一性。
📒 文章:
引用(&)
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
李白 <=> 李太白 青莲居士 诗仙 ……
🌰 栗子:
1 | void foo(int& a) |
- 引用必须初始化且不能为空。
- 引用不能更换目标。
- 一个变量可以有多个引用。
- 引用不占用额外的内存。
- 引用类型必须和引用实体是同种类型的。
常引用
1 | void TestConstRef() |
使用场景
做参数
1
2
3
4
5
6void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}做返回值
1
2
3
4
5int& TestRefReturn(int& a)
{
a += 10;
return a;
}
⚠️
1 | int& Add(int a, int b) |
如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型
返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。
传值和传引用(作为参数 / 作为返回值)在效率上的差距!
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
1
2
3
4
5
6
7
8
9
10// a.cpp
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}查看汇编代码:
保留编译过程中生成的临时文件:
g++ a.cpp -save-temps
其中
a.s
就是汇编文件,在 VS 里面可以 DEBUG 起来,直接看汇编代码,比较方便。引用在定义时必须初始化,指针没有要求。
没有NULL引用,但有NULL指针。
在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数( 32 位平台下占 4
个字节) 。
引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小。
有多级指针,没有多级引用。
访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
引用比指针使用起来相对更安全。
内联函数(inline)
以 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
以下是没有加 inline 的汇编代码。
在 Add 前加了 inline 后,在编译期间编译器会用函数体替换函数的调用。
inline 是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环 / 递归的函数不适宜使
用作为内联函数。inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// F.h
using namespace std;
inline void f(int i);
// F.cpp
void f(int i)
{
cout << i << endl;
}
// main.cpp
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
auto(C++11)
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11 中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
🌰 栗子:
1 | int TestAuto() |
⚠️
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类
型。因此 auto 并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将 auto 替换为变量实际的类型。用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &。
1
2
3
4
5
6
7
8
9
10
11
12
13
14int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
1
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
auto 不能推导的场景
auto 不能作为函数的参数。
1
void fun(auto a) {}
auto 不能直接用来声明数组。
为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。
auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有 lambda 表达式等进
行配合使用。
auto 不能定义类的非静态成员变量。
实例化模板时不能使用 auto 作为模板参数。
基于范围的 for 循环(C++11)
🌰 栗子:
1 | void TestFor() |
⚠️
与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。
for 循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
```cpp
// 下面这段代码就有问题
void TestFor(int array[])
{for(auto& e : array) { cout<< e <<endl; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
## nullptr(C++11)
NULL 实际是一个宏,在传统的 C 头文件`stddef.h`中:
```c
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL 可能被定义为字面常量 0,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在
使用空值的指针时,都不可避免的会遇到一些麻烦,如下:
1 | void f(int) |
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖。 在C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0。
为了避免混淆,C++11 提供了 nullptr ,即:nullptr 代表一个指针空值常量。nullptr 是有类型的,其类型为nullptr_t ,仅仅可以被隐式转化为指针类型,nullptr_t 被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
⚠️
- 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的。
- 在 C++11 中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。