把知识记在小本本上

将零散的知识点放在一个集中的地方,不断递归重构,形成一套为己所用的知识系统。

博客首页 | 小本本首页

从 C 到 C++

  1. cout中的 c 指的是 console。

  2. endl => end line。

  3. 作用域限定符::相当于中文,表示作用域或所属关系。

  4. extern "C"

    有时候在 C++ 工程中可能需要将某些函数按照 C 的风格来编译,在代码前加 extern “C” ,意思是告诉编译器,将该函数按照C语言规则来编译。

    1
    2
    3
    4
    extern “C”
    {
    something();
    }
  5. typeid(变量名).name输出变量的类型。

  6. 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 关键字的出现就是针对这种问题的。

  1. 相同名字的 namespace 作用域相同,同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

  2. 命名空间的作用:避免名字冲突、划分逻辑单元,名字空间中的声明和定义可以分开。

  3. 名字空间可以嵌套,使用时候得一层一层的扒皮。

  4. 名字空间也可以赋值取别名,🌰 栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    namespace A1
    {
    namespace A11
    {
    namespace A12
    {
    something();
    }
    }
    }
    namespace A = A1::A11::A12;
    // A1::A11::A12::something(); 等价于 A::something();
  5. 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. 缺省参数必须从右开始设置。

    1
    /*ERROR*/ void fun(int a = 3,char b,char *c = "ahoj");
  2. 缺省参数不能在函数声明和定义中同时出现,建议声明时指定。如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

  3. 缺省值必须是常量或者全局变量。

  4. C语言不支持(编译器不支持)。

哑元

只指定类型而不指定名称的函数,占着茅坑不拉屎。

🌰 栗子:

1
2
3
4
5
6
7
8
9
void ya(int,int b)
{
cout << b << endl;
}
int main()
{
ya(10,100);
return 0;
}
  1. 兼容老版本。
  2. 支持函数重载。

重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了”,后者是“谁也赢不了” 。

函数重载:是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 / 类型 / 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。

🌰 栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void foo(){
cout << "void foo();" << endl;
}
void foo(int a){
cout << "void foo(int a);" << endl;
}
void foo(int a,int b){
cout << "void foo(int a,int b);" << endl;
}
void foo(int a,double b){
cout << "void foo(int a,double b);" << endl;
}
void foo(double a,int b){
cout << "void foo(double a,int b);" << endl;
}

int main()
{
foo();
foo(1);
foo(1 , 2);
foo(1 , 3.14);
foo(3.14 , 1);
}
  1. 同一作用域,函数名相同,参数表不同的函数。
  2. 参数表不同:
    • 参数类型不同
    • 参数个数不同
    • 参数顺序不同
  3. 重载和形参名无关。
  4. 重载和返回类型无关。
  5. 不同作用域同名函数遵循就近原则。

重载的原理

nm a.out,查看 C++ 编译器给看书取得名字:

1
2
3
4
5
6
00000001000010a0 T __Z3Maxii
0000000100000de0 T __Z3foodi
0000000100000cf0 T __Z3fooi
0000000100000d90 T __Z3fooid
0000000100000d40 T __Z3fooii
0000000100000b50 T __Z3foov

C++ 函数重载通过编译器改名实现。

名字修饰(Name Mangling)

在 C/C++ 中,一个程序要运行起来,需要经历:预处理、编译、汇编、链接。

Name Mangling 是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。

C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。

C++ 要支持函数重载、命名空间等,使得其修饰规则比较复杂,不同编译器在底层的实现方式可能都有差
异。

被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数
列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终
的名字中,就可保证名字在底层的全局唯一性。

📒 文章:

C++的函数重载

引用(&)

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

李白 <=> 李太白 青莲居士 诗仙 ……

🌰 栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void foo(int& a)
{
a++;
}

int main()
{
int a = 20;
int& b = a;
int& c = b;
cout << a << b << c << endl;
c = 10;
cout << a << b << c << endl;
cout << &a << " " << &b << " " << &c << " " << endl;

cout << "==========" << endl;
foo(a);
cout << a << endl;

return 0;
}
  1. 引用必须初始化且不能为空。
  2. 引用不能更换目标。
  3. 一个变量可以有多个引用。
  4. 引用不占用额外的内存。
  5. 引用类型必须和引用实体是同种类型的。

常引用

1
2
3
4
5
6
7
8
void TestConstRef()
{
const int a = 10;
// int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量 const int& b = 10;
double d = 12.34;
// int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d;
}

使用场景

  1. 做参数

    1
    2
    3
    4
    5
    6
    void Swap(int& left, int& right)
    {
    int temp = left;
    left = right;
    right = temp;
    }
  2. 做返回值

    1
    2
    3
    4
    5
    int& TestRefReturn(int& a)
    {
    a += 10;
    return a;
    }

⚠️

1
2
3
4
5
6
7
8
9
10
11
12
13
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
// 这段代码输出结果为一个随机值

如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型
返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。

传值和传引用(作为参数 / 作为返回值)在效率上的差距!

引用和指针的区别

  1. 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

  2. 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

    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 起来,直接看汇编代码,比较方便。

  3. 引用在定义时必须初始化,指针没有要求。

  4. 没有NULL引用,但有NULL指针。

  5. 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数( 32 位平台下占 4

    个字节) 。

  6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小。

  7. 有多级指针,没有多级引用。

  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

  9. 引用比指针使用起来相对更安全。

内联函数(inline)

以 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。

以下是没有加 inline 的汇编代码。

在 Add 前加了 inline 后,在编译期间编译器会用函数体替换函数的调用。

  1. inline 是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环 / 递归的函数不适宜使
    用作为内联函数。

  2. inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。

  3. inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // F.h
    #include <iostream>
    using namespace std;
    inline void f(int i);

    // F.cpp
    #include "F.h"
    void f(int i)
    {
    cout << i << endl;
    }

    // main.cpp
    #include "F.h"
    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
2
3
4
5
6
7
8
int TestAuto()
{
return 20;
}
auto b = 10;
auto c = 'a';
auto d = TestAuto();
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化

⚠️

  1. 使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类
    型。因此 auto 并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将 auto 替换为变量实际的类型。

  2. 用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int 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;
    }
  3. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

    1
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同

auto 不能推导的场景

  1. auto 不能作为函数的参数。

    1
    void fun(auto a) {}
  2. auto 不能直接用来声明数组。

  3. 为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。

  4. auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有 lambda 表达式等进

    行配合使用。

  5. auto 不能定义类的非静态成员变量。

  6. 实例化模板时不能使用 auto 作为模板参数。

基于范围的 for 循环(C++11)

🌰 栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
{
e *= 2;
}
for(auto e : array)
{
cout << e << " ";
}
return 0;
}

⚠️

  1. 与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。

  2. for 循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。

  3. 1
    2
    3
    4
    5
    6
    7
    8
    // 下面这段代码就有问题
    void TestFor(int array[])
    {
    for(auto& e : array)
    {
    cout<< e <<endl;
    }
    }

nullptr(C++11)

NULL 实际是一个宏,在传统的 C 头文件stddef.h中:

1
2
3
4
5
6
7
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

NULL 可能被定义为字面常量 0,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在
使用空值的指针时,都不可避免的会遇到一些麻烦,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void f(int) 
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}

程序本意是想通过 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;

⚠️

  1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的。
  2. 在 C++11 中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。