C++入门——00预备篇

发布于:2024-08-17 ⋅ 阅读:(29) ⋅ 点赞:(0)

1.C/C++

C语言和C++在多个方面存在差异。虽然C++是C语言的超集,并且两者之间有许多相似之处,但C++引入了许多新的特性和概念。

C语言是面向过程的,关注的是过程分析出求解问题的步骤,通过函数调用逐步解决问题
C++是基于面向对象的,关注的是对象将一件事情拆分成不同的对象,靠对象之间的交互完成

以下是一些主要的区别:

1. 编程范式

  • C语言:主要是一种过程化编程语言,侧重于函数和数据的操作。
  • C++:支持过程化编程,也支持面向对象编程(OOP),包括类、继承、多态和封装等概念。

2. 数据抽象

  • C语言:不支持数据抽象。数据结构如 struct 是简单的聚合数据,但没有封装和方法。
  • C++:通过类和对象实现数据抽象。类不仅包含数据成员,还可以包含成员函数,用于操作这些数据。

3. 内存管理

  • C语言:内存管理完全由程序员控制,通过 mallocfree 函数进行动态内存分配和释放。
  • C++:除了 mallocfree,还引入了 newdelete 运算符用于动态内存管理。newdelete 与对象的构造和析构有关,更适合面向对象编程。

4. 函数重载和运算符重载

  • C语言:不支持函数重载和运算符重载。
  • C++:支持函数重载(同名函数根据参数类型和数量的不同进行区分)和运算符重载(自定义运算符的行为)。

5. 模板

  • C语言:不支持模板。
  • C++:支持模板,可以定义泛型函数和泛型类,以实现类型无关的代码。

6. 标准库

  • C语言:标准库主要包括基本的输入/输出、字符串处理、数学计算和内存操作等功能。
  • C++:标准库包括C语言的所有标准库功能,还引入了STL(标准模板库),提供了丰富的容器(如 vector, list, map)、算法(如 sort, find)和迭代器等。

7. 异常处理

  • C语言:没有内置的异常处理机制。错误处理通常通过返回值和 errno 实现。
  • C++:支持异常处理,通过 try, catchthrow 语句来处理异常情况。

8. 命名空间

  • C语言:没有命名空间机制,所有的标识符在全局范围内都是唯一的。
  • C++:支持命名空间(namespace),可以将相关的类、函数和变量组织在一起,避免命名冲突。

9. 构造函数和析构函数

  • C语言:没有构造函数和析构函数。初始化和清理工作由函数显式处理。
  • C++:支持构造函数和析构函数。构造函数在对象创建时自动调用,析构函数在对象销毁时自动调用。

10. 多态

  • C语言:不支持运行时多态。
  • C++:支持运行时多态,通过虚函数和基类指针/引用实现。

2.C++关键字

  1. alignas:指定对象对齐要求。
  2. alignof:获取对象对齐要求。
  3. and:与运算符(&& 的替代)。
  4. and_eq:按位与赋值运算符(&= 的替代)。
  5. asm:嵌入汇编代码(不推荐使用,现代 C++ 更倾向于使用 __asm)。
  6. atomic_cancel:用于原子操作(C++20)。
  7. atomic_commit:用于原子操作(C++20)。
  8. atomic_noexcept:用于原子操作(C++20)。
  9. auto:自动类型推断。
  10. bitand:按位与运算符(& 的替代)。
  11. bitor:按位或运算符(| 的替代)。
  12. bool:布尔类型。
  13. break:跳出循环或 switch 语句。
  14. case:定义 switch 语句中的一个分支。
  15. catch:捕捉异常。
  16. char:字符类型。
  17. char16_t:16 位字符类型(C++11)。
  18. char32_t:32 位字符类型(C++11)。
  19. class:定义类。
  20. compl:按位取反运算符(~ 的替代)。
  21. concept:定义模板概念(C++20)。
  22. const:常量。
  23. consteval:编译时求值函数(C++20)。
  24. constexpr:编译时常量。
  25. const_cast:常量转换。
  26. continue:跳过当前循环的剩余部分,进入下一次循环。
  27. co_await:协程的等待操作(C++20)。
  28. co_return:协程的返回操作(C++20)。
  29. co_yield:协程的生成操作(C++20)。
  30. decltype:声明类型。
  31. default:指定类的默认构造函数、拷贝构造函数等。
  32. delete:释放动态分配的内存。
  33. do:do-while 循环。
  34. double:双精度浮点类型。
  35. dynamic_cast:动态类型转换。
  36. else:if-else 分支中的 else
  37. enum:枚举类型。
  38. explicit:显式构造函数或转换运算符。
  39. export:导出模板(C++20,标准中不再推荐使用)。
  40. extern:声明外部链接的变量或函数。
  41. false:布尔值 false
  42. float:单精度浮点类型。
  43. for:for 循环。
  44. friend:友元函数或类。
  45. goto:跳转到标签。
  46. if:条件判断。
  47. import:模块导入(C++20)。
  48. inline:内联函数。
  49. int:整型。
  50. long:长整型。
  51. mutable:允许修改的成员变量。
  52. namespace:定义命名空间。
  53. new:动态分配内存。
  54. noexcept:声明函数不会抛出异常。
  55. nullptr:空指针常量。
  56. operator:重载运算符。
  57. or:逻辑或运算符(|| 的替代)。
  58. or_eq:按位或赋值运算符(|= 的替代)。
  59. private:私有访问修饰符。
  60. protected:受保护访问修饰符。
  61. public:公共访问修饰符。
  62. register:建议将变量存储在寄存器中(不推荐使用,现代编译器通常忽略)。
  63. reinterpret_cast:重新解释类型转换。
  64. requires:定义模板要求(C++20)。
  65. return:函数返回值。
  66. short:短整型。
  67. signed:有符号类型。
  68. sizeof:获取对象或类型的大小。
  69. static:静态变量或函数。
  70. static_assert:编译时断言(C++11)。
  71. struct:定义结构体。
  72. switch:switch 语句。
  73. template:定义模板。
  74. this:指向当前对象的指针。
  75. thread_local:线程局部存储(C++11)。
  76. throw:抛出异常。
  77. true:布尔值 true
  78. try:异常处理块的开始。
  79. typedef:定义类型别名。
  80. typeid:获取类型信息。
  81. typename:声明模板参数类型。
  82. union:定义联合体。
  83. unsigned:无符号类型。
  84. using:使用别名或导入名称。
  85. virtual:虚函数。
  86. void:无类型。
  87. volatile:易变变量。
  88. wchar_t:宽字符类型。
  89. while:while 循环。
  90. xor:按位异或运算符(^ 的替代)。
  91. xor_eq:按位异或赋值运算符(^= 的替代)。

3.命名空间

在 C++ 中,命名空间(namespace)是一种组织代码的机制,用于将标识符(如变量、函数、类等)分组,以避免命名冲突,并提高代码的可维护性。命名空间允许你将相关的代码放在一起,使得同一个项目中不同部分的代码可以有相同的标识符而不会产生冲突。

3.1定义命名空间

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

3.1.1普通的命名空间

namespace N1
{
    // 命名空间中的内容,既可以定义变量,也可以定义函数
    int a;
    
    int Add(int left, int right)
    {
        return left + right;
    }
}

3.1.2嵌套命名空间

namespace N2
{
    int a;
    int b;
    
    int Add(int left, int right)
    {
        return left + right;
    }
    
    namespace N3
    {
        int c;
        int d;
        
        int Sub(int left, int right)
        {
            return left - right;
        }
    }
}

3.1.3合并相同名称的命名空间

namespace N1
{
    int a;
    
    int Add(int left, int right)
    {
        return left + right;
    }
}


//同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中

namespace N1
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}

3.2命名空间的使用

#include <iostream>

namespace Math {
    int add(int a, int b) {
        return a + b;
    }
}

namespace Physics {
    int add(int a, int b) {
        return a + b + 100;  // 模拟不同的加法操作
    }
}

using Math::add;
using namespace Physics;

int main() {
    //std::cout << "Math::add(2, 3) = " << Math::add(2, 3) << std::endl;
    std::cout << "Math::add(2, 3) = " << add(2, 3) << std::endl;
    std::cout << "Physics::add(2, 3) = " << Physics::add(2, 3) << std::endl;
    return 0;
}
  • 加命名空间名称及作用域限定符  Math::add(2, 3)
  • 使用using将命名空间中成员引入  using Math::add;
  • 使用using namespace 命名空间名称引入 using namespace Physics;

4.C++输入&输出

在 C++ 中,输入和输出(I/O)是通过标准库提供的流(stream)类来实现的。最常用的 I/O 流是 iostream 头文件中的 cincoutcerrclog。下面是有关 C++ 输入和输出的详细介绍:

4.1. 基本的输入和输出

标准输出cout)用于向控制台输出数据。

标准输入cin)用于从控制台读取数据。

标准错误cerr)用于输出错误信息,通常没有缓冲机制。

标准日志clog)用于输出日志信息,通常有缓冲机制。

4.1.1输出到控制台
#include <iostream>

int main() {
    int a = 10;
    std::cout << "The value of a is: " << a << std::endl;
    return 0;
}

  • std::cout 是用于输出的流对象。
  • << 是插入运算符,将数据发送到流中。
  • std::endl 用于输出换行符并刷新流。
4.1.2从控制台读取输入
#include <iostream>

int main() {
    int a;
    std::cout << "Enter an integer: ";
    std::cin >> a;  // 从控制台读取输入
    std::cout << "You entered: " << a << std::endl;
    return 0;
}

  • std::cin 是用于输入的流对象。
  • >> 是提取运算符,从流中读取数据。

4.2. 格式化输出

C++ 提供了多种方式来格式化输出:

使用 iomanip 进行格式化
#include <iostream>
#include <iomanip>  // 需要包含该头文件

int main() {
    double pi = 3.14159265358979;

    std::cout << "Default: " << pi << std::endl;

    // 设置输出精度
    std::cout << "Precision: " << std::setprecision(4) << pi << std::endl;

    // 固定小数点格式
    std::cout << "Fixed: " << std::fixed << std::setprecision(4) << pi << std::endl;

    // 设置宽度和对齐
    std::cout << "Width: |" << std::setw(10) << pi << "|" << std::endl;

    // 填充字符
    std::cout << "Fill: |" << std::setfill('*') << std::setw(10) << pi << "|" << std::endl;

    return 0;
}

  • std::setprecision(n):设置浮点数的精度。
  • std::fixed:以固定小数点格式输出浮点数。
  • std::setw(n):设置字段宽度。
  • std::setfill(c):设置填充字符。

4.3. 错误处理

标准错误输出cerr)用于报告错误信息,不会被缓冲。

#include <iostream>

int main() {
    int a;
    std::cout << "Enter an integer: ";
    if (!(std::cin >> a)) {
        std::cerr << "Error: Invalid input." << std::endl;
        return 1;
    }
    std::cout << "You entered: " << a << std::endl;
    return 0;
}

标准日志输出clog)用于输出日志信息,通常会被缓冲。

#include <iostream>

int main() {
    std::clog << "This is a log message." << std::endl;
    return 0;
}

5.缺省参数

C++ 中的缺省参数(默认参数)允许在函数声明中为某些参数提供默认值,从而使得函数调用时可以省略这些参数的值。这样可以使函数更灵活,减少重载的需求。

#include <iostream>
using namespace std;

void TestFunc(int a = 0) {
    cout << a << endl;
}

int main() {
    TestFunc();  // 没有传参时,使用参数的默认值
    TestFunc(10); // 传参时,使用指定的实参
    return 0;
}

5.1全缺省参数

void TestFunc(int a = 10, int b = 20, int c = 30) {
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

5.2半缺省参数

void TestFunc(int a, int b = 10, int c = 20) {
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

注意:

  • 半缺省参数必须从右往左依次来给出,不能间隔着给
  •  缺省参数不能在函数声明和定义中同时出现
//a.h
void TestFunc(int a = 10);

// a.c
void TestFunc(int a = 20)
{}

// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
个缺省值。
  •  缺省值必须是常量或者全局变量

  • C语言不支持(编译器不支持)

6.函数重载

函数重载(Function Overloading)是 C++ 中一种允许在同一作用域内定义多个同名函数但具有不同参数列表的特性。函数重载使得同一函数名可以用于不同的功能,从而提高了代码的可读性和复用性。

6.1. 函数重载的基本规则

  1. 函数名相同: 重载函数必须具有相同的函数名。

  2. 参数列表不同: 函数的参数列表(参数的数量或类型)必须不同。参数列表不同可以是:

    • 参数的数量不同。
    • 参数的类型不同。
    • 参数的顺序不同(如果类型不同)。
  3. 返回类型不影响重载: 返回类型不作为函数重载的依据。即使返回类型不同,如果参数列表相同,则不能重载。

int Add(int left, int right) {
    return left + right;
}

//类型不同
double Add(double left, double right) {
    return left + right;
}

//类型不同
long Add(long left, long right) {
    return left + right;
}

int main()
{
    Add(10, 20);
    Add(10.0, 20.0);
    Add(10L, 20L);
    return 0;
}

6.2为什么C++支持函数重载,而C语言不支持函数重载呢?

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

在linux下,gcc编译后的函数修饰后名字不变。

在linux下,g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中【_Z+函数长度+函数名+类型首字母】。

  • int Add(int a,int b)  =======> <_Z3Addii>
  • int func(int a,double b,int* p)  =======> <_Z4funcidPi>

这里我们就可以看出,用返回值不同是没办法进行函数重载的。

6.3 extern “C”

在 C++ 中,extern "C" 是一个用于指示编译器使用 C 语言的链接方式来编译和链接代码的关键字。它的主要作用是让 C++ 代码可以与 C 语言代码互操作,特别是为了在 C++ 程序中调用 C 语言编写的函数或被 C 语言代码调用。

6.3.1. 为什么需要 extern "C"

由于 C++ 支持函数重载,编译器在编译 C++ 代码时会对函数名进行名字修饰(Name Mangling),而 C 语言不支持函数重载,不进行名字修饰。因此,C 语言编译器和 C++ 编译器生成的函数名是不一样的。extern "C" 的作用就是告诉 C++ 编译器对指定的代码块或函数采用 C 语言的链接方式,即不进行名字修饰,从而保证函数名一致,使得 C 和 C++ 代码可以互相调用。

extern "C" int Add(int left, int right);

int main() {
    Add(1, 2);
    return 0;
}

7.引用(起别名)

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

类型& 引用变量名(对象名) = 引用实体,注意:引用类型必须和引用实体是同种类型的

void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

7.1引用特点

  • 必须初始化: 引用在定义时必须被初始化,并且不能被重新赋值为另一个变量的引用。
  • 不可为空: 引用必须绑定到某个合法的对象或变量,不能是空的。
  • 绑定后不可更改: 一旦引用绑定到某个变量后,就无法再改变引用所指向的对象。
void TestRef()
{
    int a = 10;
    // int& ra; // 该条语句编译时会出错
    int& ra = a;
    int& rra = a;
    printf("%p %p %p\n", &a, &ra, &rra);
}

7.2 常引用

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;
}

7.2引用的常见用法

7.2.1 作为函数参数

通过引用传递参数可以避免复制参数,特别是对于大型对象或数组,效率更高:

void increment(int& num) {
    num++;
}

int main() {
    int x = 10;
    increment(x);
    std::cout << x << std::endl;  // 输出 11
}

在这个例子中,increment 函数接收一个 int 类型的引用参数 num,直接修改了传入的变量 x 的值。

7.2.2 作为函数返回值

函数可以返回一个引用,这样可以在函数调用结束后仍然引用某个对象:

int& getX() {
    static int x = 10;
    return x;
}

int main() {
    int& ref = getX();
    ref = 20;  // 修改了 getX 返回的 x
    std::cout << getX() << std::endl;  // 输出 20
}

注意:返回局部变量的引用会导致未定义行为,因为局部变量在函数返回后会被销毁,所以通常函数返回的引用要么是静态变量,要么是全局变量。

7.2.3 引用常量(const 引用)

const 引用是对常量的引用,它允许你通过引用访问数据,但不能修改它。这在需要通过引用传递参数而不想让函数修改参数时非常有用:

void printValue(const int& num) {
    std::cout << num << std::endl;
}

int main() {
    int x = 10;
    printValue(x);  // 可以传递普通变量
    printValue(20);  // 可以传递字面值常量
}

7.3 传值、传引用效率比较

1. 传值(Pass by Value)

传值时,函数接收的是参数的一个副本。也就是说,函数内部对参数的修改不会影响原始数据。传值的过程涉及到将实际参数的值复制一份到函数的形参中。

2. 传引用(Pass by Reference)

传引用时,函数接收的是参数的引用,这意味着函数操作的是原始数据本身,而不是它的副本。传引用可以通过引用或指针实现。

3. 效率比较

传值的效率:

  • 对于基本类型,传值的效率通常非常高,因为复制操作的开销几乎可以忽略不计。
  • 对于大型对象,传值的效率较低,因为需要进行深拷贝,这可能涉及到大量的内存分配和数据复制操作。

传引用的效率:

  • 传引用几乎总是更高效,因为它避免了对象的复制。传递引用的开销仅限于传递一个指针的大小(通常是 4 字节或 8 字节,取决于系统架构)。
  • 对于大型对象或容器类(如 std::vectorstd::string 等),传引用能显著减少内存和时间开销。

7.4 引用和指针的区别

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

7.1. 基本概念

  • 引用(Reference): 是某个变量的别名。引用一旦被初始化后,就与该变量绑定在一起,无法再指向其他变量。引用通过变量的名称进行访问,但实际上访问的是绑定的原始变量。

  • 指针(Pointer): 是一个存储变量地址的变量。指针可以指向任意一个变量,并且可以在程序运行时修改指向的对象。

7.2. 声明和初始化

  • 引用 必须在声明时进行初始化,并且在初始化后无法改变指向的对象。

    int a = 5;
    int& ref = a; // 引用 ref 被初始化为变量 a 的别名
    

  • 指针 可以在声明时未初始化(即空指针),并且可以随时修改指向的地址。

    int a = 5;
    int* ptr = &a; // 指针 ptr 存储了变量 a 的地址
    

7.3. 使用方式

  • 引用 通过普通变量的方式访问,不需要使用解引用操作符 *

    ref = 10; // 直接修改 a 的值为 10
    

  • 指针 需要通过解引用操作符 * 来访问或修改指向的对象。

    *ptr = 10; // 修改 a 的值为 10
    

7.4. 修改指向的对象

  • 引用 一旦初始化,就无法改变其绑定的对象,即引用始终指向同一个变量。

    int b = 20;
    ref = b; // 这只是将 b 的值赋给 a,ref 仍然引用 a
    

  • 指针 可以随时修改指向的对象,可以指向不同的变量。

    int b = 20;
    ptr = &b; // 现在 ptr 指向 b 而不是 a
    

7.5. 空值

  • 引用 不能为 nullptr(空引用),它必须引用一个有效的变量。

  • 指针 可以为 nullptr,表示不指向任何有效对象。

    int* ptr = nullptr; // 指针没有指向任何对象
    

7.6. 运算能力

  • 指针 支持指针运算,可以进行加法、减法操作来遍历数组或内存块。

    int arr[3] = {1, 2, 3};
    int* ptr = arr;
    ptr++; // 指针现在指向 arr[1]
    

  • 引用 不支持任何指针运算。

7.7. 内存地址

  • 引用 隐藏了内存地址的概念,直接操作引用就等同于操作其绑定的变量。

  • 指针 显式地展示了内存地址,通过指针可以直接访问或操作内存。

7.8. 语法和使用场景

  • 引用 通常用于函数参数传递(传引用)和返回值,它更加安全且容易理解,常用于需要保证指向不变的场景。

    void increment(int& num) {
        num++; // 直接修改传入的变量
    }
    

  • 指针 更加灵活,可以在动态内存分配、数组处理、实现复杂数据结构(如链表、树)时使用。

     
    int* ptr = new int(10); // 动态分配内存
    delete ptr; // 释放内存
    

7.9. 语言级支持

  • 引用 是 C++ 的特性,C 语言中没有引用的概念。

  • 指针 是 C 语言和 C++ 共有的特性,C++ 的指针功能是从 C 继承而来。

7.10. 总结

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3. 没有NULL引用,但有NULL指针
  4.  在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全

8.内联函数

内联函数(Inline Function)是 C++ 中的一种用于提高程序执行效率的功能。通过将函数定义为内联函数,程序可以避免在函数调用时的开销,从而加快函数的执行速度。

在 C++ 中,内联函数是通过在函数定义前加上 inline 关键字来声明的:

inline int add(int a, int b) {
    return a + b;
}

函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。

8.1特性

  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
/ 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 中被引用

8.2 宏的优缺点?

优点:
        1.增强代码的复用性。
        2.提高性能。
缺点:
        1.不方便调试宏。(因为预编译阶段进行了替换)
        2.导致代码可读性差,可维护性差,容易误用。
        3.没有类型安全的检查 。

8.3C++有哪些技术替代宏?

1. 常量定义 换用const
2. 函数定义 换用内联函数
 

9.auto关键字(C++11)

auto 关键字是 C++11 引入的一种功能,它允许编译器根据变量的初始化表达式自动推导变量的类型。使用 auto 可以简化代码,减少手动编写类型的麻烦,尤其是在类型复杂或不容易确定的情况下。

int TestAuto()
{
    return 10;
}
int main()
{
    int a = 10;
    auto b = a;
    auto c = 'a';
    auto d = TestAuto();
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    cout << typeid(d).name() << endl;
    //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
    return 0;
}

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

8.1 auto的使用细则

8.1.1. auto与指针和引用结合起来使用

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

int main()
{
    int x = 10;
    
    auto a = &x;    // a 被推导为 int* 类型
    auto* b = &x;   // b 被推导为 int* 类型
    auto& c = x;    // c 被推导为 int& 类型
    
    cout << typeid(a).name() << endl;   // 输出 a 的类型
    cout << typeid(b).name() << endl;   // 输出 b 的类型
    cout << typeid(c).name() << endl;   // 输出 c 的类型
    
    *a = 20;   // 通过指针 a 修改 x 的值
    *b = 30;   // 通过指针 b 修改 x 的值
    c = 40;    // 通过引用 c 修改 x 的值
    
    return 0;
}

8.1.2 在同一行定义多个变量

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

void TestAuto()
{
    auto a = 1, b = 2;
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

8.2 auto不能推导的场景

8.2.1. auto不能作为函数的参数

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

8.2.2auto不能直接用来声明数组

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}

8.2.3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
8.2.4. auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。

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

10.1 范围for的语法

在C++98中如果要遍历一个数组,可以按照以下方式进行:

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
        array[i] *= 2;
    for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
        cout << *p << endl;
}

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)
        e *= 2;
    for(auto e : array)
        cout << e << " ";
return 0;
}

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

10.2范围for的使用条件

10.2.1. 确定的迭代范围

要使用基于范围的 for 循环,容器或范围必须能够明确地确定其开始和结束。这意味着,容器需要实现以下接口或特性:

  • 数组:数组的范围是从第一个元素到最后一个元素。
  • 标准库容器:如 std::vectorstd::liststd::map 等,它们提供了 begin()end() 方法来确定迭代范围。
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}

错误示范

#include <iostream>

int main() {
    int* ptr = nullptr;
    
    // 错误:ptr 的范围不明确
    for (int x : ptr) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

在上面的错误示例中,ptr 不提供确定的范围,因此无法使用范围-based for 循环。

10.2.2迭代对象的操作

迭代器(或范围的元素)必须支持以下操作:

  • 解引用操作符 *:通过解引用操作符来访问当前元素。
  • 前缀递增操作符 ++:用于移动到下一个元素。
  • 等于操作符 ==:用于比较迭代器或元素,以确定是否到达容器的末尾。
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    
    // 使用范围-based for 循环遍历 std::list
    for (int x : lst) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

11.指针空值nullptr(C++11)

11.1 C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
    int* p1 = NULL;
    int* p2 = 0;
    // ......
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

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

void f(int)
{
    cout<<"f(int)"<<endl;
}

void f(int*)
{
    cout<<"f(int*)"<<endl;
}

int main()
{
    f(0);            //输出 f(int)
    f(NULL);         //输出 f(int)(因为 NULL 被视为 0)
    f((int*)NULL);   //输出 f(int*)(因为 (int*)NULL 是 int* 类型)。
    return 0;
}


在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

注意:

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