Modern C++
创始人
2025-05-31 08:03:25

1. 迈向现代C++

1.1 被启用的特性

注意:弃用并非彻底不能用,只是用于暗示程序员这些特性将从未来的标准中消失,应该尽量避免使用。但是,已弃用的特性依然是标准库的一部分,并且出于兼容性的考虑,大部分特性其实会『永久』保留。

  • 不允许字符串常量赋值给char *。如果需要用字符串常量赋值和初始化一个char *,应该使用const char * 或者auto
char *str = "hello world"; //将出现弃用警告
  • C++98异常说明,unexpected_handlerset_unexpected() 等相关特性被弃用,应该使用 noexcept
  • auto_ptr被弃用,应该使用unique_ptr

1.2 与C的兼容性

出于一些不可抗力、历史原因,我们不得不在 C++ 中使用一些 C 语言代码(甚至古老的 C 语言代码),例如 Linux 系统调用。在现代 C++ 出现之前,大部分人当谈及『C 与 C++ 的区别是什么』时,普遍除了回答面向对象的类特性、泛型编程的模板特性外,就没有其他的看法了,甚至直接回答『差不多』,也是大有人在。图 1.2 中的韦恩图大致上回答了 C 和 C++ 相关的兼容情况。
在这里插入图片描述
在这里插入图片描述
C++不是C的一个超集,这个观念从一开始就是错的!
在编写C++代码的时候,应该尽量避免使用void*之类的程序风格,在不得已使用C的时候,应该用extern "C"这种特性,将C++代码和C语言的代码分离编译,再统一链接。

// foo.h
#ifdef __cplusplus
extern "C" {
#endifint add(int x, int y);#ifdef __cplusplus
}
#endif// foo.c
int add(int x, int y) {return x+y;
}// 1.1.cpp
#include "foo.h"
#include 
#include int main() {[out = std::ref(std::cout << "Result from C code: " << add(1, 2))](){out.get() << ".\n";}();return 0;
}

应该先使用gcc编译C语言的代码
gcc -c foo.c
编译出foo.o的文件,再用g++/clang++将C++代码和.o文件进行链接(或者都编译成为.o文件再统一进行链接):
clang++ 1.1.cpp foo.o -std=c++2a -o 1.1

2. 语言可用性的强化

当我们声明、定义一个变量或者常量,对代码进行流程控制、面向对象的功能、模板编程等这些都是运行时之前,可能发生在编写代码或编译器编译代码时的行为。为此,我们通常谈及语言可用性,是指那些发生在运行时之前的语言行为。

2.1 常量

nullptr

nullptr是为了替代NULL。传统C++会把NULL0视为同一种东西,这要取决于编译器如何定义NULL,有些编译器是直接定义成0或者((void*)0)
C++不允许直接将void *隐式转换成其他类型,但是如果编译器尝试把(void*0),那么在下面这句代码中:

char *ch = NULL;

没有了 void * 隐式转换的 C++ 只好将 NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:

void foo(char*);
void foo(int);

那么foo(NULL)将会调用void foo(int),反人类。
因此,C++11引入了nullptr专门用来区分空指针和0。nullptr类型为nullptr_t,能够隐式转换成任何指针或成员指针的类型,也可以和他们之间比较相等或不等。

#include 
#include void foo(char *);
void foo(int);int main() {if (std::is_same::value)std::cout << "NULL == 0" << std::endl;if (std::is_same::value)std::cout << "NULL == (void *)0" << std::endl;if (std::is_same::value)std::cout << "NULL == nullptr" << std::endl;foo(0);          // 调用 foo(int)foo(nullptr);    // 调用 foo(char*)return 0;
}void foo(char *) {std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {std::cout << "foo(int) is called" << std::endl;
}

输出:
foo(int) is called
foo(char*) is called

constexpr

C++本身已经具备了常量表达式的概念,如1+2,3x4这种表达式会产生相同的结果并且没有副作用。如果编译器能够在编译的时候能将这些表达式直接优化并植入程序运行时,能够增加程序的性能。如:

#include 
#define LEN 10int len_foo() {int i = 2;return i;
}
constexpr int len_foo_constexpr() {return 5;
}constexpr int fibonacci(const int n) {return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}int main() {char arr_1[10];                      // 合法char arr_2[LEN];                     // 合法int len = 10;// char arr_3[len];                  // 非法const int len_2 = len + 1;constexpr int len_2_constexpr = 1 + 2 + 3;// char arr_4[len_2];                // 非法char arr_4[len_2_constexpr];         // 合法// char arr_5[len_foo()+5];          // 非法char arr_6[len_foo_constexpr() + 1]; // 合法std::cout << fibonacci(10) << std::endl;// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55std::cout << fibonacci(10) << std::endl;return 0;
}

在上面的例子中,char arr_4[len_2]比较迷惑,因为len_2已经被定义成了一个常量。
为什么仍然是非法的?这是因为C++标准中数组的长度必须是一个常量表达式,而这个变量是const常数,而不是一个常量表达式。因此(现在很多编译器支持这种行为)这是非法行为,我们需要使用C++引入constexor特性来解决这个问题;对于arr_5来说,C++98之前的编译器无法得知函数在运行期间返回的实际是一个常数,因此也导致了非法行为的产生。
C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。
此外,constexpr 修饰的函数可以使用递归:

constexpr int fibonacci(const int n) {return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

从 C++14 开始,constexpr函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:

constexpr int fibonacci(const int n) {if(n == 1) return 1;if(n == 2) return 1;return fibonacci(n-1) + fibonacci(n-2);
}

const和constexpr的作用和区别

对于变量来说

  • const表示这个变量无法修改,但是并没有限定这个变量时编译期间的常量还是运行期间的常量
  • constexpr表示这个变量只能是编译期间的常量
const int kSize = 1; // 编译期常量void func()
{const int kRandomNumber = get_a_random_number(); // 运行期常量......
}

对于 kSize,你既可以用 const 修饰,也可以用 constexpr。但对于 kRandomNumber,你只能用 const。
对于函数来说

  • const修饰的一般都是成员函数,用来表示这个函数不会对成员变量产生写的操作
  • constexpr实际是const expression,即常量表达式
constexpr int func(int i)
{return i + 1;
}int main()
{int i             = 10;const int ci      = 10;constexpr int cei = 10;std::array   arr1; // 编译错误std::array  arr2; // 没问题std::array arr3; // 没问题std::array  arr4; // 没问题func(i); // 直接使用,也是没问题的return 0;
}

constexpr修饰的函数,简单来说,如果传入的参数可以再编译期间算出来,那么这个函数就产生编译时期的值。但是如果传入的参数不能在编译期间算出,那么该关键字修饰的函数,就和普通函数一样,比如上面代码,直接调用了fun(i)。不过我们不必因此写两个版本。

2.2 变量及其初始化

if/switch变量声明强化

在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量 int,但始终没有办法在 ifswitch 语句中声明一个临时的变量。例如:

#include 
#include 
#include int main() {std::vector vec = {1, 2, 3, 4};// 在 c++17 之前const std::vector::iterator itr = std::find(vec.begin(), vec.end(), 2);if (itr != vec.end()) {*itr = 3;}// 需要重新定义一个新的变量const std::vector::iterator itr2 = std::find(vec.begin(), vec.end(), 3);if (itr2 != vec.end()) {*itr2 = 4;}// 将输出 1, 4, 3, 4for (std::vector::iterator element = vec.begin(); element != vec.end(); ++element)std::cout << *element << std::endl;
}

在上面的代码中,我们可以看到 itr 这一变量是定义在整个 main() 的作用域内的,这导致当我们需要再次遍历整个 std::vector 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:

// 将临时变量放到 if 语句内
if (const std::vector::iterator itr = std::find(vec.begin(), vec.end(), 3);itr != vec.end()) {*itr = 4;
}

初始化列表

初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。 在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、 POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体) 类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。 而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。 这些不同方法都针对各自对象,不能通用。例如:

#include 
#include class Foo {
public:int value_a;int value_b;Foo(int a, int b) : value_a(a), value_b(b) {}
};int main() {// before C++11int arr[3] = {1, 2, 3};Foo foo(1, 2);std::vector vec = {1, 2, 3, 4, 5};std::cout << "arr[0]: " << arr[0] << std::endl;std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl;for (std::vector::iterator it = vec.begin(); it != vec.end(); ++it) {std::cout << *it << std::endl;}return 0;
}

为解决这个问题,C++11 首先把初始化列表的概念绑定到类型上,称其为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

#include 
#include 
#include class MagicFoo {
public:std::vector vec;MagicFoo(std::initializer_list list) {for (std::initializer_list::iterator it = list.begin();it != list.end(); ++it)vec.push_back(*it);}
};
int main() {// after C++11MagicFoo magicFoo = {1, 2, 3, 4, 5};std::cout << "magicFoo: ";for (std::vector::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) std::cout << *it << std::endl;
}

这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。
初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:

public:void foo(std::initializer_list list) {for (std::initializer_list::iterator it = list.begin();it != list.end(); ++it) vec.push_back(*it);}magicFoo.foo({6,7,8,9});

其次,C++11 还提供了统一的语法来初始化任意的对象,例如:

Foo foo2 {3, 4};

结构化绑定

结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。

C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:

#include 
#include std::tuple f() {return std::make_tuple(1, 2.3, "456");
}int main() {auto [x, y, z] = f();std::cout << x << ", " << y << ", " << z << std::endl;return 0;
}

std::tuple 和 std::tie的基本用法

  • 定义和初始化
tuple t3 = {1, 2.0, "3"};
  • 访问
    可以使用get<常量表达式>(tuple_name)来访问或修改tuple的元素(返回引用)
    get<0>(t3) = 4;cout << get<1>(t3) << endl;
  • 批量赋值
    std::tie会将变量的引用整合成一个tuple,从而实现批量赋值。
    int i; double d; string s;tie(i, d, s) = t3;cout << i << " " << d << " " << s << endl;
  • 还可以使用std::ignore忽略某些tuple中的某些返回值,如
tie(i, ignore, s) = t3;
  • 需要注意的是,tie无法直接从初始化列表获得值,比如下面第5行会编译错误。
    int i; double d; string s;tuple t3 = {1, 2.0, "3"};tie(i, d, s) = t3;t3 = {1, 2.0, "3"};tie(i, d, s) = {1, 2.0, "3"};

2.3 类型推导

在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。

C++11 引入了 autodecltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。

auto

使用auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到了传统 C++ 中冗长的迭代写法:

// 在 C++11 之前
// 由于 cbegin() 将返回 vector::const_iterator
// 所以 it 也应该是 vector::const_iterator 类型
for(vector::const_iterator it = vec.cbegin(); it != vec.cend(); ++it)//After C++··
#include 
#include 
#include class MagicFoo {
public:std::vector vec;MagicFoo(std::initializer_list list) {// 从 C++11 起, 使用 auto 关键字进行类型推导for (auto it = list.begin(); it != list.end(); ++it) {vec.push_back(*it);}}
};
int main() {MagicFoo magicFoo = {1, 2, 3, 4, 5};std::cout << "magicFoo: ";for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {std::cout << *it << ", ";}std::cout << std::endl;return 0;
}

一些其他的常见用法:

auto i = 5;              // i 被推导为 int
auto arr = new auto(10); // arr 被推导为 int *

从 C++ 20 起,auto 甚至能用于函数传参,考虑下面的例子:

int add(auto x, auto y) {return x+y;
}
auto i = 5; // 被推导为 int
auto j = 6; // 被推导为 int
std::cout << add(i, j) << std::endl;

注意:auto 还不能用于推导数组类型:

auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto'auto auto_arr2[10] = {arr};

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:decltype(表达式)
有时候,我们可能需要计算某个表达式的类型,例如:

auto x = 1;
auto y = 2;
decltype(x+y) z;

你已经在前面的例子中看到 decltype 用于推断类型的用法,下面这个例子就是判断上面的变量x, y, z是否是同一类型:

if (std::is_same::value)std::cout << "type x == int" << std::endl;
if (std::is_same::value)std::cout << "type x == float" << std::endl;
if (std::is_same::value)std::cout << "type z == type x" << std::endl;

其中,std::is_same 用于判断 TU 这两个类型是否相等。输出结果为:

type x == int
type z == type x

尾返回类型推导

你可能会思考,在介绍 auto 时,我们已经提过 auto 不能用于函数形参进行类型推导,那么 auto 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:

template
R add(T x, U y) {return x+y;
}

注意:typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename 消除歧义

这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 add() 这个函数会做什么样的操作,以及获得一个什么样的返回类型。

在 C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:

decltype(x+y) add(T x, U y)

但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

template
auto add2(T x, U y) -> decltype(x+y){return x + y;
}

令人欣慰的是从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:

template
auto add3(T x, U y){return x + y;
}

可以检查一下类型推导是否正确:

// after c++11
auto w = add2(1, 2.0);
if (std::is_same::value) {std::cout << "w is double: ";
}
std::cout << w << std::endl;// after c++14
auto q = add3(1.0, 2);
std::cout << "q: " << q << std::endl;

decltype(auto)

decltype(auto) 是 C++14 开始提供的一个略微复杂的用法。
简单来说,decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:

std::string  lookup1();
std::string& lookup2();

在 C++11 中,封装实现是如下形式:

std::string look_up_a_string_1() {return lookup1();
}
std::string& look_up_a_string_2() {return lookup2();
}

而有了 decltype(auto),我们可以让编译器完成这一件烦人的参数转发:

decltype(auto) look_up_a_string_1() {return lookup1();
}
decltype(auto) look_up_a_string_2() {return lookup2();
}

2.4 控制流

if constexpr

正如本章开头出,我们知道了 C++11 引入了 constexpr 关键字,它将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件,考虑下面的代码:

#include template
auto print_type_info(const T& t) {if constexpr (std::is_integral::value) {return t + 1;} else {return t + 0.001;}
}
int main() {std::cout << print_type_info(5) << std::endl;std::cout << print_type_info(3.14) << std::endl;
}

在编译时,实际代码就会表现为如下:

int print_type_info(const int& t) {return t + 1;
}
double print_type_info(const double& t) {return t + 0.001;
}
int main() {std::cout << print_type_info(5) << std::endl;std::cout << print_type_info(3.14) << std::endl;
}

区间for迭代

#include 
#include 
#include int main() {std::vector vec = {1, 2, 3, 4};if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;for (auto element : vec)std::cout << element << std::endl; // read onlyfor (auto &element : vec) {element += 1;                      // writeable}for (auto element : vec)std::cout << element << std::endl; // read only
}

2.5 模板

C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之一。

外部模板

相关内容

热门资讯

美股收盘:三大指数集体收涨 纳... 财联社12月20日讯(编辑 夏军雄)美东时间周五,在甲骨文和英伟达走强的带动下,人工智能(AI)概念...
贵阳有轨电车T2线主体硬件基本... 转自:贵州日报 本报讯(记者 冷赛楠)近日,“天眼问政”栏目收到网友留言:贵阳有轨电车T2线从比亚迪...
大山“用水三变” 转自:贵州日报 “以前靠山上小水源灌田,纯靠天吃饭。现在水龙头就在边上,要浇地直接打开就行。”69岁...
危房抢险施工公告 转自:贵州日报 贵阳市云岩区鸿雁巷17、19、20号及弯弓街6号住户:为消除贵阳市云岩区鸿雁巷17、...
“请3休8”带旺元旦假期云南旅...   本报讯 首席记者李思凡报道 “请3休8”带旺元旦游。12月19日,飞猪发布的数据显示,2026年...