学习Cpp-从C到Cpp

从了解 Cpp 已经有一点时间了,然后就转用了 Python 和 Vue 做项目。

好久不见,甚是想念。

命名空间

避免命名冲突。

需要注意的几点:

  1. 同一个工程中允许存在多个相同名称的命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace A {
    int aa = 1;
    int ab = 2;
    }

    namespace A {
    // int ab = 3; // 提示已存在变量 ab
    int ac = 4;
    }
  2. 命名空间可以嵌套

  3. 如下写法使用的是全局的变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>

    int a = 10;

    int main() {
    int a = 20;
    std::cout << ::a << std::endl; // 输出 10

    return 0;
    }

缺省参数

缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默 认值,否则使用指定的实参。

全缺省参数、半缺省参数。

注意:

  1. 缺省参数不能在声明和定义中同时出现。

    WHY?

    如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

    在看完《高质量 C++/C 编程指南》后,我觉得下面的写法是比较舒服的:

    1
    2
    3
    4
    5
    6
    7
    // 声明
    void func(int i=1, double d=1.0);

    // 定义
    void func(int i, double d) {
    std::cout << i << std::endl << d << std::endl;
    }
  2. 半缺省参数必须从右向左依次给出。

    1
    2
    3
    4
    5
    // 错误
    void func(int i = 0, int j, int k = 1) {}

    // 正确
    void func(int i, int j = 0, int k = 1) {}

    WHY?

    我想了一会儿,难道又是王八的屁股?后来明白了~看下面的代码:

    1
    void func(int a, int b, int c, int d = 10);

    我在调用 func 的时候是怎么给参数赋值的呢?对,从左往右写。

    半缺省参数这样规定就可以让我们在调用传参的时候很方便的空出后面已经有了默认参数的参数。半缺省参数从左往右给的话就会有歧义。

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

    WHY?

    缺省值是变量编译器就无法在编译的时候确定值。

函数重载

同名函数的形参列表(参数个数/类型/顺序)必须不同。

WHY?

这个和 C++ 编译器对函数的名字修饰有关。

extern “C”

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

1
2
3
4
5
6
7
8
9
10
#include <iostream>

extern "C" int Add(int x, int y) {
return x + y;
}

int main() {
std::cout << Add(1, 2) << std::endl;
return 0;
}

引用

我李逵,江湖人称黑旋风。

引用的底层(在 VS 中可以 debug 模式下看汇编代码)其实和指针的实现方式是一致的。

注意:

  1. 引用类型必须和引用实体是同类型的(就像别人叫你🐖🐖,你也不答应一样,人家 int a,你说人家是 double d,你经过 a 的监护人编译器的同意了吗)。
  2. 引用在定义时必须初始化。
  3. 引用一旦引用一个实体,再不能引用其他实体。
  4. 一个变量可以有多个引用。

常引用

看看下面几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1
const int a = 10;
int &ar = a; // 报错
const int &ar = a; // ok

// 2
int b = 20;
int &rb1 = b; // ok
const int &rb2 = b; // ok

// 3
int c = 30;
double &rc1 = c; // 报错
const double &rc2 = c; // ok

先来看看 3,报错是因为类型不同,那为什么下面那一句加了 const 就可以了呢?

WHY?

如果将 rc1 写成double rc1 = c;是可以的,中间发生了隐式类型转换,隐式类型转换的过程中会生成一个临时的空间 temp,这个临时变量是只读的(如果写成double &rc1 = c;就变成可读可写的了,所以会报错),这个过程大致是这样子的:

62A37143-98D9-44E2-8ED3-032451FBAC7F.png

总结一下:

不能赋值的根本原因是因为权限超过了原有的权限,权限降低是允许的,但是超越最初权限范围是错误的。/\:)

引用的使用场景

做函数参数、做函数的返回值。

注意:

不能返回栈上面的变量。

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

1
2
3
4
5
6
7
8
9
10
11
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;
}

传值和传引用的效率差距

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实
参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。举 2 个栗子,可以从输出看出效率:

栗子 1:

1
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
28
29
30
31
32
struct A {
int data[10000];
};

void func1(A a) {
}

void func2(A &a) {
}

int main() {
A a;

size_t begin = clock();
for (size_t i = 0; i < 10000; ++i)
func1(a);
size_t end = clock();
std::cout << "func1(A a) : " << end - begin << std::endl;

begin = clock();
for (size_t i = 0; i < 10000; ++i)
func2(a);
end = clock();
std::cout << "func2(A a) : " << end - begin << std::endl;

return 0;
}

/* 输出
func1(A a) : 36976
func2(A &a) : 22
*/

栗子 2:

1
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
28
29
30
31
32
33
34
struct A {
int data[10000];
};

A a;

A func1() {
return a;
}

A& func2() {
return a;
}

int main() {
size_t begin = clock();
for (size_t i = 0; i < 100000; ++i)
func1();
size_t end = clock();
std::cout << "A func1() : " << end - begin << std::endl;

begin = clock();
for (size_t i = 0; i < 100000; ++i)
func2();
end = clock();
std::cout << "A& func2() : " << end - begin << std::endl;

return 0;
}

/* 输出
A func1() : 174202
A& func2() : 264
*/

指针和引用异同

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

    1
    2
    3
    4
    5
    6
    7
    int main() {
    int a = 10;
    int& ra = a;
    cout<<"&a = "<<&a<<endl;
    cout<<"&ra = "<<&ra<<endl;
    return 0;
    }
  2. 底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int main() {
    int a = 10;

    int& ra = a;
    ra = 20;

    int* pa = &a;
    *pa = 20;
    return 0;
    }

    汇编代码

引用和指针的不同点:

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

  2. 引用在初始化时引用一个实体后,不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实

    体。

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

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

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

  6. 有多级指针,但是没有多级引用。

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

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

内联函数

使用 inline 修饰的函数。编译时编译器会在调用处将内联函数展开,没有函数压栈的开销,内联函数提升程序运行的效率。

注意:

  1. inline 函数是一种以空间换时间的方法,省去额外调用函数的开销,有循环或者递归的函数不适宜作为内联函数。
  2. inline 对于编译器来说只是一个建议,至于编译器听不听你的就看情况了,编译器觉得你傻他就信自己的。
  3. inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找 不到。

E9EBA8AC-8FDA-4ADC-AAB3-A192C3D4CE80.png

1DC0FD1A-E413-474E-9F72-5312C7D7FC3D.png

auto 关键字(C++11)

在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人 去使用它。

C++11 中 auto 不再是一个存储类型指示符,而是作为一个新的类型指 示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

注意:

  1. 使用 auto 定义变量时必须对其进行初始化。

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

  2. auto 与指针结合起来使用。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int x = 10;

    auto a = &x;
    auto *b = &x;
    auto &c = x;

    *a = 20;
    *b = 30;
    c = 40;
  3. 在同一行定义多个变量。

    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义后面的变量。

    1
    auto c = 3, d = 4.0;  // error!
  4. auto 不能作为函数的参数。

  5. auto 不能直接用来声明数组。

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

  7. auto 在实际中最常见的优势用法就是和 for 循环、lambda 表达式等进行配合使用。

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

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

基于范围 for(C++11)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int arr[] = {1, 2, 3, 4, 5};
for (auto &e : arr) {
e *= 2;
}

for (auto e : arr) {
cout << e << endl;
}

// error! arr 已经是作为指针传入了,不能确定其范围。
void func(int arr[]) {
for (auto &e : arr) {
cout << e << endl;
}
}

注意:

  1. for 循环的迭代范围必须是确定的。

    对于数组而言,就是数组中第一个元素和最后一个元素的范围。

    对于类而言,类应该提供 begin 和 end 方法来确定循环范围。

  2. 迭代的对象要实现 ++ 和 == 操作。

nullptr(C++11)

NULL 可能被定义为字面常量 0,或者被定义为无类型指针 void * 的常量。

文章作者: ahoj
文章链接: https://ahoj.cc/2019/09/学习Cpp-从C到Cpp/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ahoj 的小本本