【C++ 学习 ①】- C++ 入门知识(上万字详解)

目录

一、历史

1.1 - C 语言的发展历程

1.2 - C++ 发展历程

二、C++ 关键字(C++98)

三、命名空间

3.1 - 命名空间的定义

3.2 - 命名空间的使用

四、C++ 输入&输出

五、缺省参数

5.1 - 缺省参数的概念

5.2 - 缺省参数的分类

六、函数重载

6.1 - 函数重载的概念

6.2 - C++ 是如何做到函数重载的

七、引用

7.1 - 引用的概念

7.2 - 引用作为函数参数

7.3 - 引用作为函数返回值

7.4 - 常引用

7.5 - 引用和指针

八、内联函数

8.1 - 为什么要使用内联函数

8.2 - 内联函数和宏

8.3 - 将内联函数定义在头文件中

8.4 - 慎用内联

九、auto 关键字(C++11) 

9.1 - 进行类型推导

9.2 - 使用细则

9.3 - 适用场景

9.4 - 不适用场景

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

10.1 - 语法

10.2 - 使用条件

十一、指针空指 nullptr(C++11)


创作不易,可以点点赞,如果能关注一下博主就更好了~

参考资料

  1. C语言的历史 - 知乎 (zhihu.com)

  2. C++_百度百科 (baidu.com)

  3. 详解c++的命名空间namespace - 知乎 (zhihu.com)

  4. C++ std命名空间 - 知乎 (zhihu.com)

  5. C++函数重载详解 (biancheng.net)

  6. C++引用精讲,C++ &用法全面剖析 (biancheng.net)

  7. C语言基础篇 (二十三) 运算中的临时匿名变量

  8. C++内联函数的使用 - 博客园 (cnblogs.com)

  9. 【C++】内联函数为什么定义在头文件中?

  10. [C++11] auto关键字详解

  11. C语言丨一文带你了解auto关键字(又名隐形刺客) - 知乎 (zhihu.com)


一、历史

1.1 - C 语言的发展历程

20 世纪 60 年代,AT&T 贝尔实验室的研究员 肯·汤普森(Ken Thompson) 发明了 B 语言,并使用 B 语言编写了一个名为 Space Travel 的游戏。

AT&T 是 American Telephone & Telegraph 的简称,即美国电话电报公司,由亚历山大·贝尔于 1877 年创立。

贝尔拥有电话的发明专利,但是有人指出,从意大利移民到美国的安东尼奥·梅乌奇(Antonio Meucci)才是电话的发明者。美国国会 2002 年 6 月 15 日 265 号决议确认安东尼奥·梅乌奇为电话的发明人。

他想玩这个游戏,所以背着老板找到了一台空闲的机器 PDP-7,但是由于这台机器没有操作系统,汤普森于是着手为 PDP-7 开发操作系统,后来这个 OS 被命名为 UNIX

丹尼斯·里奇(Dennis Richie)也想玩这个游戏,所以加入了汤普森,合作开发 UNIX,他的主要工作是改进汤普森的 B 语言,最后产生了一个新的语言,即 C 语言

C 语言源自 B 语言,而 B 语言源自 BCPL 语言,因此取 BCPL 的第二个字母,也就是 B 的下一个字母。

1973 年,肯·汤普森和丹尼斯·里奇用 C 语言重写了 UNIX。

肯·汤普森(左)与丹尼斯·里奇

1.2 - C++ 发展历程

20 世纪 70 年代中期,本贾尼·斯特劳斯特卢普(Bjarne Stroustrup) 在剑桥大学计算机中心工作。本贾尼希望开发一个既要编程简单、正确可靠,又要运行高效、可移植性的计算机程序设计语言,而以 C 语言为背景,以 Simula 思想为基础的语言,正好符合他的初衷和设想。

Simula 被认为是第一个面向对象的编程语言。

1979 年,本贾尼到了 AT&T 实验室,开始从事将 C 改良为带类的 C(C with classes)的工作。1983 年,该语言被正式命名为 C++(C Plus Plus)

C++ 的历史版本:

阶段内容
C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等
C++1.0添加虚函数概念,函数和运算符重载,引用、常量等
C++2.0更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及 const 成员函数
C++3.0进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理
C++98C++ 标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写 C+ +标准库,引入了 STL(Standard Template Library,标准模板库)
C++03C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性
C++05C++标准委员会发布了一份技术报告(Technical Report,TR1),正式更名 C++0x,即:计划在本世纪第一个 10 年的某个时间发布
C++11增加了许多特性,使得 C++ 更像一种新语言,比如:正则表达式、基于范围 for 循环、auto 关键字、新容器、列表初始化、标准线程库等
C++14对 C++11 的扩展,主要是修复 C++11 中漏洞以及改进,比如:泛型的 lambda 表达式, auto 的返回值类型推导,二进制字面常量等
C++17在 C++11上 做了一些小幅改进,增加了 19 个新特性,比如:static_assert() 的文本信息可选,Fold 表达式用于可变的模板,if 和 switch 语句中的初始化器等
C++20C++11 以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程 (Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更 新:比如 Lambda 支持模板、范围 for 支持初始化等
C++23制定 ing

C++ 是基于 C 语言而产生的,C++ 既可以进行 C 语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。


二、C++ 关键字(C++98)

C 语言有 32 个关键字,而 C++ 总共有 63 个关键字

asm auto 
bool break
case catch char class const const_cast continue
delete do double dynamic_cast default
else enum explicit export extern
false float for friend
goto
if inline int long
mutable
namespace new
operator
private protected public
reinterpret_cast return register
short signed sizeof static static_cast struct switch
template this try typedef typeid typename throw true
union unsigned using
virtual void volatile
wchar_t while


三、命名空间

在 C++ 中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称相互冲突的可能性就越大,另外使用多个厂商的类库时,也可能会导致名称冲突。为了避免在大规模程序的设计中,以及在程序员使用各种 C++ 库时,这些标识符的命名发生冲突,标准 C++ 引入关键字 namespace(命名空间),可以更好地控制标识符的作用域。

3.1 - 命名空间的定义

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

#include <stdio.h>
​
// 分别定义了名字为 A 和 B 的命名空间
// 一般开发中是使用项目名作为命名空间的名字
namespace A
{
    int i = 0;
}
​
namespace B
{
    int i = 1;
}
​
int i = 2;
​
int main()
{
    int i = 3;
    // 局部域 -> 全局域
    printf("%d\n", i);  // 3
    // :: 是作用域限定符,前面是空白则表示全局域
    printf("%d\n", ::i);  // 2
    printf("%d %d\n", A::i, B::i);  // 0 1
}
​
// 注意:命名空间只能在全局范围内定义(下面为错误写法)
//int main()
//{
//  namespace C
//  {
//      int i = 2;
//  }
//  return 0;
//}

命名空间可以嵌套

#include <stdio.h>
​
namespace A
{
    int i = 0;
    namespace B
    {
        int i = 1;
    }
}
​
int main()
{
    printf("%d %d\n", A::i, A::B::i);  // 0 1
    return 0;
}

命名空间是开放的,可以随时把新的成员加入已有的命名空间中(常用)

#include <stdio.h>
​
namespace A
{
    int i = 0;
}
​
namespace A
{
    int j = 1;
}
​
int main()
{
    printf("%d %d", A::i, A::j);  // 0 1
    return 0;
}

命名空间中的函数可以在命名空间之外定义

#include <stdio.h>
​
namespace A
{
    int i = 0;
    int func(int x, int y);
}
​
// 成员函数在外部定义的时候,需要加作用域
int A::func(int x, int y)
{
    printf("%d\n", i);  // 访问命名空间的数据不用加作用域
    return x + y;
}
​
int main()
{
    printf("%d\n", A::func(10, 20));  // 30
    return 0;
}

3.2 - 命名空间的使用

命名空间有以下三种使用方式:

  1. 命名空间名称 + 作用域限定符(::) + 成员名

  2. 使用关键字 using 将命名空间中的某个成员引入

    #include <stdio.h>
    ​
    namespace A
    {
        int i = 0;
    }
    ​
    using A::i;
    ​
    int main()
    {
        printf("%d\n", i);  // 0
        return 0;
    }
  3. 使用 using namespcae 展开命名空间

    #include <stdio.h>
    ​
    namespace A
    {
        int i = 0;
    }
    ​
    using namespace A;
    ​
    int main()
    {
        printf("%d\n", i);  // 0
        return 0;
    }

    直接展开命名空间会有风险,例如全局域中有一个同名的全局变量 i,那么将命名空间 A 展开后,就会出现冲突(i 不明确)。

    因此不推荐在项目开发中直接展开命名空间,在日常练习中则可以那样操作。


四、C++ 输入&输出

#include <iostream>
using namespace std;  // 在日常练习中可以使用,在项目开发中则不建议使用
​
int main()
{
    int number = 0;
    // << 是流插入运算符
    // >> 是流提取运算符
    cout << "Enter a decimal number:" << endl;
    cin >> number;
    cout << "The number you enterd is " << number << "." << endl;
    return 0;
}

std 是 C++ 标准库的命名空间名。

早期标准库将所有功能定义在全局域中,声明在 .h 后缀的头文件中,使用时只需要包含对应头文件即可。后来标准 C++ 引入了命名空间的概念,并将标准库中的内容封装到了 std 命名空间中,同时为了不与原来的头文件混淆,规定标准 C++ 使用一套新的头文件,这套头文件的文件名不加 .h 扩展名。

并不是写了 #include <iostream> 就必须使用 using namespace std;,这样写的原因通常是为了把 std 命名空间中的内容暴露到全局域中(就像直接包含了 iostream.h 这种没有命名空间的头文件一样),使标准 C++ 库用起来与传统 iostream.h 一样方便。但不建议这样做,因为使用 using namespace std; 的话就没有起到命名空间的作用,再次回到了如同没有涉及命名空间时,所有标识符都定义在全局作用域中的混乱情况,不利于程序员创建新对象。


五、缺省参数

5.1 - 缺省参数的概念

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

缺省的英文是 default,也就是默认的意思。

#include <iostream>
using namespace std;
​
void func(int n = 0)
{
    cout << n << endl;
}
​
int main()
{
    func();  // 0
    func(10);  // 10
    return 0;
}

5.2 - 缺省参数的分类

  1. 全缺省参数

    #include <iostream>
    using namespace std;
    ​
    void func(int a = 10, int b = 20, int c = 30)
    {
        cout << a <<  ", " << b << ", " << c << endl;
    }
    ​
    int main()
    {
        func();  // 10, 20, 30
        func(1);  // 1, 20, 30
        func(1, 2);  // 1, 2, 30
        func(1, 2, 3);  // 1, 2, 3
        return 0;
    }
  2. 半缺省参数

    #include <iostream>
    using namespace std;
    ​
    void func(int a, int b = 20, int c = 30)
    {
        cout << a << ", " << b << ", " << c << endl;
    }
    ​
    int main()
    {
        func(1);  // 1, 20, 30
        func(1, 2);  // 1, 2, 30
        func(1, 2, 3);  // 1, 2, 3
        return 0;
    }

    注意

    1. 半缺省参数必须从右往左依次给出,且不能间隔着给

    2. 缺省参数不能在函数声明和定义中同时出现,否则编译器无法确定到底该用哪个缺省值

    3. Stack.h

      #pragma once
      ​
      // 顺序栈
      typedef int SDataType;
      ​
      typedef struct Stack
      {
          SDataType* data;
          int top;
          int capacity;
      }Stack;
      ​
      // 基本操作
      void StackInit(Stack* pst, int default_capacity);

      Stack.cpp

      #include "Stack.h"
      #include <assert.h>
      #include <stdlib.h>
      ​
      void StackInit(Stack* pst, int default_capacity = 5)
      {
          assert(pst);
          pst->data = (SDataType*)malloc(sizeof(SDataType) * default_capacity);
          if (NULL == pst->data)
          {
              perror("initialization failed!");
              exit(-1);
          }
          pst->top = 0;
          pst->capacity = default_capacity;
      }

      test.cpp

      #include <Stack.h>
      ​
      int main()
      {
          Stack st;
          StackInit(&st, 4);  // ok
          // StackInit(&st);  // error --> 编译时出错,因为函数调用中的参数太少
          // 所以这种情况下,缺省参数应该出现在函数声明中,而不是定义中
          return 0;
      }


六、函数重载

6.1 - 函数重载的概念

函数重载(function overloading)指的是在同一个作用域内(同一个类、同一个命名空间等)有多个名称相同但参数列表不同的函数。函数重载的结果是让一个函数名拥有了多种用途,使得命名更加方便,调用更加灵活。

参数列表包括参数的类型参数的个数参数的顺序,只要有一个不同就叫作参数列表不同(注意:进仅仅是参数不同是不可以的)。

函数的返回值类型不能作为重载的依据

#include <iostream>
using namespace std;
​
void Swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}
​
void Swap(float* x, float* y)
{
    float tmp = *x;
    *x = *y;
    *y = tmp;
}
​
int main()
{
    int i1 = 10, i2 = 20;
    Swap(&i1, &i2);
    cout << i1 << ", " << i2 << endl;  // 20, 10
​
    float f1 = 12.5, f2 = 22.5;
    Swap(&f1, &f2);
    cout << f1 << ", " << f2 << endl;  // 22.5, 12.5
    return 0;
}

函数重载和缺省参数

#include <iostream>
using namespace std;
​
void test()
{
    cout << "test()" << endl;
}
​
void test(int a = 0)
{
    cout << "test(int a)" << endl;
}
​
int main()
{
    test(10);  // ok --> 即这两个同名的函数构成重载
    // test();  // error --> 对重载函数的调用不明确
    return 0;
}

6.2 - C++ 是如何做到函数重载的

在 C/C++ 中,一个程序要运行起来,需要经过编译链接的过程。

其中编译又具体分为预编译(预处理)编译汇编这三个过程。

test.c

#include <stdio.h>
​
extern int Add(int x, int y);
​
int main()
{
    int a = 10;
    int b = 20;
    int sum = Add(a, b);
    printf("%d\n", sum);
    return 0;
}

add.c

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

当链接器看到 test.o 调用 Add,但是没有 Add 的地址,就会到 add.o 的符号表中去找 Add 的地址,然后链接到一起

那么链接时,面对 Add 函数,链接器会使用哪个名字去找呢?每个编译器都有自己的函数名修饰规则,由于 Windows 下的 VS 的修饰规则过于复杂,而 Linux 下的 gcc 和 g++ 修饰规则简单易懂,因此下面分别使用 gcc 和 g++ 进行演示。

  1. 采用 C 语言编译器(gcc)编译后的结果

    可以看出 gcc 编译器并没有对函数名做修改

  2. 采用 C++ 编译器(g++)编译后的结果

    可以看出 g++ 编译器的函数名修饰规则为:_Z + 函数名长度 + 函数名 + 参数类型首字母

总结:由于 C 语言编译器在编译时并没对函数名做修改,无法区分同名函数,所以不支持函数重载;而 C++ 通过函数修改规则对同名函数进行了区分,只要参数列表不同,修饰出来的函数名就不同,所以支持函数重载


七、引用

7.1 - 引用的概念

引用(Reference)是 C++ 相对于 C 语言的又一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号、笔名等,使用绰号、笔名和本名都能表示一个人。

引用的特性

  1. 引用在定义时必须初始化

  2. 引用一旦引用一个实体,则再不能引用其他实体

  3. 一个变量可以有多个引用

#include <iostream>
using namespace std;
​
int main()
{
    // 一个变量可以有多个引用
    int a = 10;
    int& ra = a;
    int& rra = ra;
    cout << &a << " " << &ra << " " << &rra << endl;  // 输出的三个地址相同
    
    // 通过引用修改原始变量中所存储的数据
    ++ra;
    ++rra;
    cout << a << " " << ra << " " << rra << endl;  // 12 12 12
    
    // 引用一旦引用一个实体,则再不能引用其他实体
    int x = 20;
    ra = x;  // 将 x 的值赋给 ra
    cout << a << " " << ra << " " << rra << endl;  // 20 20 20
    return 0;
}

引用在定义时需要添加 &,在使用时不能添加 &,使用时添加 & 表示取地址运算符或按位与运算符。

7.2 - 引用作为函数参数

在定义或声明函数时,可以将函数的参数指定为引用的形式,这样调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有 "在函数内部影响函数外部数据" 的效果。

#include <iostream>
using namespace std;
​
void Swap(int& x, int& y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
​
int main()
{
    int a = 10;
    int b = 20;
    Swap(a, b);
    cout << a << " " << b << endl;  // 20 10
    return 0;
}

以值作为函数参数,形参是实参的一份临时拷贝;以引用作为函数参数则可以避免复制对象,提高效率,尤其是对于大对象的实参

#include <iostream>
#include <time.h>
using namespace std;
​
#define N 1000
​
struct A
{
    int arr[N];
};
​
void TestVal(A a) {}
​
void TestRef(A& a) {}
​
void TestValAndRefTime()
{
    A a = { 0 };
    // 以值作为函数参数
    int begin1 = clock();
    for (int i = 0; i < 1000000; ++i)
    {
        TestVal(a);
    }
    int end1 = clock();
    // 以引用作为函数参数
    int begin2 = clock();
    for (int i = 0; i < 1000000; ++i)
    {
        TestRef(a);
    }
    int end2 = clock();
​
    cout << "TestValTime: " << end1 - begin1 << endl;  // 98
    cout << "TestRefTime: " << end2 - begin2 << endl;  // 24
}
​
int main()
{
    TestValAndRefTime();
    return 0;
}

7.3 - 引用作为函数返回值

#include <iostream>
using namespace std;
​
int& Count()
{
    static int n = 0;
    ++n;
    cout << &n << endl;
    return n;
}
​
int main()
{
    // 情形一
    // int ret = Count();
    // cout << &ret << endl;  // 输出的两个地址不同
    // cout << ret << endl;  // 1
    
    // 情形二
    int& ret = Count();  // ret 此时是静态局部变量 n 的别名
    cout << &ret << endl;  // 输出的两个地址相同
    cout << ret << endl;  // 1
    return 0;
}

情形一类似于

int a = 10;
int& ra = a;
int b = ra;

情形二类型于

int a = 10;
int& ra = a;
int& rra = ra;

在将引用作为函数返回值时应该注意,不要返回局部变量等临时变量的引用

// 错误示例
int& Add(int x, int y)
{
    int sum = x + y;
    return sum;
}

以值作为函数的返回值,在返回期间,函数不会直接将变量本身返回,而是返回变量的一份临时拷贝;以引用作为函数返回值可以避免复制对象,提高效率,尤其是对于大对象的返回值

#include <iostream>
#include <time.h>
using namespace std;
​
#define N 1000
​
struct A
{
    int arr[N];
};
​
A a;
​
A TestVal() { return a; }
​
A& TestRef() { return a; }
​
void TestValAndRefTime()
{
    // 以值作为函数返回值
    int begin1 = clock();
    for (int i = 0; i < 1000000; ++i)
    {
        TestVal();
    }
    int end1 = clock();
    // 以引用作为函数返回值
    int begin2 = clock();
    for (int i = 0; i < 1000000; ++i)
    {
        TestRef();
    }
    int end2 = clock();
​
    cout << "TestVal-Time: " << end1 - begin1 << endl;  // 167
    cout << "TestRef-Time: " << end2 - begin2 << endl;  // 23
}
​
int main()
{
    TestValAndRefTime();
    return 0;
}

7.4 - 常引用

如果不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制,这种引用方式为常引用

#include <iostream>
using namespace std;
​
int func()
{
    static int x = 0;
    cout << &x << endl;
    return x;
}
​
int main()
{
    int a = 10;
    int& b = a; // ok(权限平移)
    const int& c = a;  // ok(权限缩小)
​
    const int x = 20;
    // int& y = x;  // error(权限放大)
    const int& y = x;  // ok
​
    // func 返回的是一个临时变量,临时变量具有常性
    // int& ret = func();  // error
    const int& ret = func();  // ok
    cout << &ret << endl;  // 输出的两个地址不同
    return 0;
}

C语言基础篇 (二十三) 运算中的临时匿名变量

7.5 - 引用和指针

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

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

int main()
{
    // 指针
    int a = 10;
    int* pa = &a;
    *pa = 20;
​
    // 引用
    int b = 20;
    int& rb = b;
    rb = 40;
    return 0;
}

引用和指针的汇编代码对比

引用和指针的不同点

  1. 引用概念上定义一个变量的别名,指针存储一个变量的地址;

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

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

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

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

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

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

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

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


八、内联函数

8.1 - 为什么要使用内联函数

在 C++ 中我们通常定义以下函数来求两个整数的最大值:

int max(int x, int y)
{
    return x > y ? x : y;
}

为这么一个小的操作定义一个函数的好处有:

  1. 阅读和理解 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多;

  2. 如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多;

  3. 使用函数可以确保统一的行为,每个测试都保证以相同的方式实现;

  4. 函数可以重写,不必为其他应用程序重写代码。

虽然有这么多好处,但是写成函数有一个潜在的缺点,即调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作,例如调用函数前要保存寄存器,并在返回时恢复,可能需要拷贝实参,程序转向新的位置执行等。

C++ 中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 修饰的函数叫作内联函数,内联函数通常就是将它在程序中的每个调用点上 "内联地" 展开,例如:

inline int max(int x, int y)
{
    return x > y ? x : y;
}

那么调用:cout << max(20, 10) << endl;

在编译时展开为:cout << 20 > 10 ? 20 : 10 << endl;

8.2 - 内联函数和宏

  1. 宏容易出错

    在 C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来像函数。编译预处理器用拷贝宏代码的方式取代函数调用,省去参数压栈、生成汇编语言的 CALL 调用、返回参数、执行 return 等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在拷贝宏代码时常常产生意想不到的边际效应。例如:

    #define MAX(X, Y) (X) > (Y) ? (X) : (Y)

    语句 int result = MAX(20, 10) + 30; 将被预处理器扩展为 int result = (20) > (10) ? (20) : (10) + 30;,由于算术运算符 "+" 的优先级比条件运算符 "?:" 高,所以 result 的结果并不等价于预期的 50,而是 20。

    如果把宏代码改写为:

    #define MAX(X, Y) ((X) > (Y) ? (X) : (Y))

    则可以解决由优先级引起的错误,但是即使使用修改后的宏代码也不是万无一失的,例如语句 int i = 20; int j = 10; int result = MAX(i++, j); 将被预处理器解释为 int result = ((i++) > (j) ? (i++) : (j));,那么 result 的结果为 21,i 被修改为 22。

  2. 宏不可调试

    宏的另一个缺点就是不可调试,但是内联函数是可以调试的。内联函数不是也像宏一样进行代码展开吗?怎么能够调试呢?其实内联函数的 "可调试" 不是说它展开后还能调试,而是在程序的调试(Debug)版本里它根本就没有真正内联,编译器像普通函数那样为它生成含有调试信息的可执行代码。在程序的发行(Release)版本里,编译器才会实施真正的内联。有的编译器还可以设置函数内联开关,例如 Visual C++。

8.3 - 将内联函数定义在头文件中

  1. 对于普通函数,声明一般放在头文件中,定义则放在 .cpp 文件中

    Max.h

    #pragma once
    ​
    int max(int x, int y);

    Max.cpp

    #include "Max.h"
    ​
    int max(int x, int y)
    {
        return x > y ? x : y;
    }

    test.cpp

    #include "Max.h"
    #include <iostream>
    using namespace std;
    ​
    int main()
    {
        cout << max(20, 10) << endl;  // 20
        return 0;
    }

    在编译阶段找到函数的声明,在链接阶段去找函数的定义

  2. 对于内联函数,应该将定义放在头文件中

    Max.h

    inline int max(int x, int y)
    {
        return x > y ? x : y;
    }

    test.cpp

    #include "Max.h"
    #include <iostream>
    using namespace std;
    ​
    int main()
    {
        cout << max(20, 10) << endl;  // 20
        return 0;
    }

    在编译期间编译器需要找到内联函数的定义,然后在调用点进行展开

8.4 - 慎用内联

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

内联说明只是向编译器出发的一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个 75 行的函数也不大可能在调用点内联地展开。


九、auto 关键字(C++11) 

9.1 - 进行类型推导

在 C 语言和 C++98 标准中,auto 关键字用于修饰变量(自动存储的局部变量)。

存储类是 C 语言和 C++ 语言的标准中,变量与函数的可访问性(即作用域范围 scope)与生命周期(life time)。

存储类可分为 auto、register、static、extern、mutable、thread_local 等。

auto 存储类是所有局部变量默认的存储类。

在 C++11 中,标准委员会赋予了 auto 全新的含义,不再用于修饰变量,而是作为一个类型指示符,指示编译器在编译时推导 auto 声明的变量的数据类型

在 Linux 平台下,编译需要加 -std=c++11 参数

#include <iostream>
using namespace std;
​
int func()
{
    return 20;
}
​
int main()
{
    int a = 10;
    auto b = a;
    auto c = 'a';
    auto d = 3.14;
    auto e = func();
​
    cout << b << " " << c << " " << d << " " << e << endl;  // 10 a 3.14 20
​
    cout << typeid(b).name() << endl;  // int
    cout << typeid(c).name() << endl;  // char
    cout << typeid(d).name() << endl;  // double
    cout << typeid(e).name() << endl;  // int
​
    return 0;
}

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

9.2 - 使用细则

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

    // 用 auto 声明指针类型时,auto 和 auto* 没有任何区别,
    // 但用 auto 声明引用类型时则必须加 &
    ​
    #include <iostream>
    using namespace std;
    ​
    int main()
    {
        int a = 10;
        auto p1 = &a;
        auto* p2 = &a;
        auto& r = a;
    ​
        cout << typeid(p1).name() << endl;  // int*
        cout << typeid(p2).name() << endl;  // int*
        cout << typeid(r).name() << endl;  // int
    ​
        (*p1)++;
        (*p2)++;
        r++;
        cout << a << " " << *p1 << " " << *p2 << " " << r << endl;  // 13 13 13 13
        return 0;
    }
  2. 在同一行定义多个变量

    // 当在同一个定义多个变量时,这些变量必须是相同的类型,否则编译器会报错,
    // 因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
    ​
    int main()
    {
        auto a = 1, b = 2; // ok
        // auto c = 3, d = 4.0;  // error
        return 0;
    }

9.3 - 适用场景

不要滥用 auto,该关键字在编程时真正的用途如下

  1. 代替冗长复杂的类型。

  2. 模板函数中声明依赖模板参数的变量类型。

  3. 模板函数返回值依赖模板参数的变量类型。

  4. 用于 lambda 表达式中。

9.4 - 不适用场景

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

    int Add(auto x, auto y)
    {
        return x + y;
    }

    以上的代码会编译失败,auto 不能作为形参类型,因为编译器无法对 x 和 y 的实际类型进行推导。

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

    int arr[] = { 1, 2, 3 };  // ok
    auto num[] = { 4, 5, 6 };  // error


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

10.1 - 语法

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

#include <iostream>
using namespace std;
​
int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    int n = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < n; ++i)
    {
        arr[i] *= 2;
    }
    for (int* p = arr; p < arr + n; ++p)
    {
        cout << *p << " ";
    }
    // 2 4 6 8 10
    cout << endl;
    return 0;
}

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

#include <iostream>
using namespace std;
​
int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    for (auto& e : arr)
    {
        e *= 2;
    }
    for (auto e : arr)
    {
        cout << e << " ";
    }
    // 2 4 6 8 10
    cout << endl;
    return 0;
}

与普通循环类型,可以用 continue 来跳过本次循环,也可以用 break 来结束整个循环

10.2 - 使用条件

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

    void PrintArr(int arr[])
    {
        for (auto e : arr)
        {
            cout << e << " ";
        }
        cout << endl;
    }

    以上代码有问题,因为范围不确定。

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


十一、指针空指 nullptr(C++11)

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

int* p1 = NULL;

NULL 实际上是一个宏,在头文件中,可以看到如下代码:

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

从中可知,在 C 中 NULL 是 ((void *)0) 指针,在 C++ 中 NULL 则是字面常量 0。因此在使用空值的指针时,不可避免地会遇到一些麻烦,例如:

#include <iostream>
using namespace std;
​
void test(int)
{
    cout << "test(int)" << endl;
}
​
void test(int*)
{
    cout << "test(int*)" << endl;
}
​
int main()
{
    test(NULL);  // test(int)
    return 0;
}

在 C++ 中,如果不使用形参,则可以省略形参名

上面程序的本意是想通过 test(NULL) 调用指针版本的 test(int*) 函数,但是由于 NULL 被定义为 0,因此与程序的初衷相悖。

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

注意:

  1. 使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++ 11 作为新关键字引入的

  2. 在 C++ 11 中,sizeof(nullptr)sizeof((void *)0) 所占的字节数相同。

  3. 为了提高代码的健壮性,在后续表示指针空值时最好使用 nullptr。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值