从一个案例看 C++ 历史

求一个列表中所有数的和:

a = [4, 3, 2, 1]
print(sum(a))

C 语言

#include <stdlib.h>
#include <stdio.h>

int main() {
    std::size_t nv = 4;
    int *v = (int*)malloc(nv * sizeof(int));
    v[0] = 4;
    v[1] = 3;
    v[2] = 2;
    v[3] = 1;
    
    int sum = 0;
    for(std::size_t i = 0; i < nv; ++i)
        sum += v[i];
    printf("%d\n", sum);
    
    free(v);
    return 0;
}

C++98 引入 STL 容器库

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v(4);
    v[0] = 4;
    v[1] = 3;
    v[2] = 2;
    v[3] = 1;
    
    int sum = 0;
    for(std::size_t i = 0; i < v.size(); ++i) {
        sum += v[i];
    }
    
    std::cout << sum << std::endl;
    return 0;
}

C++11 引入了 {} 初始化表达式

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {4, 3, 2, 1};
    
    int sum = 0;
    for(std::size_t i = 0; i < v.size(); ++i) {
        sum += v[i];
    }
    
    std::cout << sum << std::endl;
    return 0;
}

C++11 引入了 range-based for-loop

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {4, 3, 2, 1};
    
    int sum = 0;
    for(int vi : v) {
        sum += vi;
    }
    
    std::cout << sum << std::endl;
    return 0;
}

C++11 引入了 lambda 表达式

#include <vector>
#include <iostream>
#include <algorithm>

int sum = 0;

void func(int vi) {
    sum += vi;
}

int main() {
    std::vector<int> v = {4, 3, 2, 1};
    
    std::for_each(v.begin(), v.end(), func);
    
    std::cout << sum << std::endl;
    return 0;
}
#include <vector>
#include <iostream>
#include <algorithm>

int main() {
    std::vector<int> v = {4, 3, 2, 1};
    
    int sum = 0;
    std::for_each(v.begin(), v.end(), [&](int vi) {
        sum += vi;
    });
    
    std::cout << sum << std::endl;
    return 0;
}

C++14 的 lambda 允许用 auto 自动推断类型

#include <vector>
#include <iostream>
#include <algorithm>

int main() {
    std::vector<int> v = {4, 3, 2, 1};
    
    int sum = 0;
    std::for_each(v.begin(), v.end(), [&](auto vi) {
        sum += vi;
    });
    
    std::cout << sum << std::endl;
    return 0;
}

C++17 CTAD

compile-time argument deduction 编译期参数推断

#include <vector>
#include <iostream>
#include <algorithm>

int main() {
    std::vector v = {4, 3, 2, 1};
    
    int sum = 0;
    std::for_each(v.begin(), v.end(), [&](auto vi) {
        sum += vi;
    });
    
    std::cout << sum << std::endl;
    return 0;
}

C++ 思想

面向对象

封装性

将多个逻辑上相关的变量包装成一个类,比如要表达一个数组,需要:起始指针 v,数组大小 nv。因此 C++ 的 vector 将他俩打包起来,避免程序员犯错。

不变性

比如当我要设置数组大小为 4 时,不能只 nv = 4。还要重新分配数组内存,从而修改数组起始地址 v。

常遇到:当需要修改一个成员时,其他成员也需要被修改,否则出错。这种情况出现时,就意味着你需要把成员变量的读写封装为成员函数

请勿滥用封装

仅当出现"修改一个成员时,其他也成员要被修改,否则出错"的现象时,才需要 getter/setter 封装。

各个成员之间相互正交,比如数学矢量类 Vec3,就没必要去搞封装,只会让程序员变得痛苦,同时还有一定性能损失:特别是如果 getter/setter 函数分离了声明和定义,实现在另一个文件时!

RAII

如果没有解构函数,则每个带有返回的分支都要手动释放所有之前的资源:

与 Java、Python 等垃圾回收语言不同,C++ 的解构函数是显式的,离开作用域自动销毁,毫不含糊(有好处也有坏处,对高性能计算而言利大于弊)。

异常安全

C++ 标准保证当异常发生时,会调用已创建对象的解构函数。因此 C++ 中没有 finally 语句。

如果此处不关闭,则可等待稍后垃圾回收时关闭。虽然最后还是关了,但如果对时序有要求或对性能有要求就不能依靠 GC。比如 mutex 忘记 unlock 造成死锁等等......

构造函数

单个参数(陷阱)

隐式转换

避免陷阱

explicit

加了 explicit 表示必须用 () 强制转换,否则 show(80) 也能编译通过!所以,如果你不希望这种隐式转换,请给单参数的构造函数加上 explicit。比如 std::vector 的构造函数 vector(size_t n) 也是 explicit 的。

Explicit 对多个参数也起作用

多个参数时,explicit 的作用体现在禁止从一个 {} 表达式初始化。

如果你希望在一个返回 Pig 的函数里用:

return {"佩奇", 80};

的话,就不要加 explicit。

顺便一提,上一个例子中 show(80)show({80}) 等价。

{} 和 () 调用构造函数

  1. int(3.14f) 不会出错,但是 int{3.14f} 会出错,因为 {} 是非强制转换;

  2. Pig("佩奇", 3.14f) 不会出错,但是 Pig{"佩奇", 3.14f} 会出错,原因同上,更安全;

  3. 可读性:Pig(1, 2) 则 Pig 有可能是个函数,Pig{1, 2} 看起来更明确。

补充说明:

针对存在二义性的 C++ 语句,只要它有可能被解释为函数声明,编辑器就肯定将其解释成函数声明。


class A {
public:
    int i;
};

int main() {
    A a();
    a.i = 2;  // 错误, A a() 被作为函数声明
}

class A {
public:
    int i;
};

int main() {
    A a{};
    a.i = 2;
}

其实谷歌在其 Code Style 中也明确提出了别再通过 () 调用构造函数,需要类型转换时应该用:

  1. static_cast<int>(3.14f) 而不是 int(3.14f)

  2. reinterpret_cast<void*>(0xb8000) 而不是 (void*)0xb8000

更加明确用的哪一种类型转换(cast),从而避免一些像是 static_cast<int>(ptr) 的错误。

补充说明:

static_cast

编译器隐式执行的任何类型转换都可以由 static_cast 来完成,比如 int 与 float、double 与 char、enum 与 int 之间的转换等;

double a = 1.999;
int b = static_cast<int>(a);

使用 static_cast 可以找回存放在 void* 指针中的值;

double a = 1.999;
void* vptr = &a;
double* dptr = static_cast<double*>(vptr);

static_cast 也可以用在于基类与派生类指针或引用类型之间的转换。然而它不做运行时的检查,不如 dynamic_cast 安全。static_cast 仅仅是依靠类型转换语句中提供的信息来进行转换,而 dynamic_cast 则会遍历整个类继承体系进行类型检查,因此 dynamic_cast 在执行效率上比 static_cast 要差一些。现在我们有父类与其派生类如下:

class ANIMAL
{
public:
    ANIMAL() : _type("ANIMAL") {};
    virtual void OutputName() { cout << "ANIMAL"; }
    
private:
    string _type;
};

class DOG: public ANIMAL
{
public:
    DOG(): _name("大黄")
         , _type("DOG")
    {};
    
    void OutputName() { cout << _name; }
    void OutputType() { cout << _type; }
    
private:
    string _name;
    string _type;
};

int main()
{
    ANIMAL* ani1 = new ANIMAL();
    DOG* dog1 = static_cast<DOG*>(ani1);
    dog1->OutputType();    // 正常执行,但是执行结果不确定;使用 dynamic_cast 会报错
    
    ANIMAL* ani3 = new DOG();
    DOG* dog3 = static_cast<DOG*>(ani3);
    dog3->OutputName();    // 正常执行,结果正确
    
    DOG* dog2 = new DOG();
    ANIMAL* ani2 = static_cast<DOG*>(dog2);
    ani2->OutputName();    // 正常执行,结果正确
}

对于对象的转换,涉及到两个方法:

  • 构造函数

  • 类型转换运算符

static_cast 会根据上述顺序寻找到合适的方法进行类型转换。

#include <iostream>

class Programmer;
class Human;
class Ape;

class Programmer {
public:
	Programmer() = default;
	
private:
	int test0;
};

class Human {
public:
	Human(Ape& a) {
		std::cout << "Human constructor, parameter is Ape" << std::endl;
	}
	operator Programmer() {
		std::cout << "Programmer Type Translation" << std::endl;
		return heart;
	}

private:
	Programmer heart;
};

class Ape {
public:
	Ape() = default;

private:
	int test1;
};

int main() {
	Ape a;
	Human h = static_cast<Human>(a);

	Programmer p;
	p = static_cast<Programmer>(h);

	return 0;
}

reinterpret_cast

reinterpret_cast 用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换,从整数类型(包括枚举类型)到指针类型,无视大小。

IBM C++ 指南给出了建议:

  • 从指针类型到一个足够大的整数类型;

  • 从整数类型或者枚举类型到指针类型;

  • 从一个指向函数的指针到另一个不同类型的指向函数的指针;

  • 从一个指向对象的指针到另一个不同类型的指向对象的指针;

  • 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针;

  • 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针。

typedef int (*FunctionPointer)(int);
int value = 21;
FunctionPointer funcP;
funcP = reinterpret_cast<FunctionPointer>(&value);
funcP(value);

编译通过,运行错误。

C++之父指出:错误的使用 reinterpret_cast 很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用 reinterpret_cast 方式。

IBM 的 C++ 指南指出:reinterpret_cast 不能像 const_cast 那样去除 const 修饰符。

int main() 
{
	typedef void (*FunctionPointer)(int);
	int value = 21;
	const int* pointer = &value;
	
	int* pointer_r = reinterpret_cast<int*>(pointer);	// error

	FunctionPointer funcP = reinterpret_cast<FunctionPointer>(pointer);		// 指向函数的指针默认带有,所指内容不可变的特性,因此编译不会报错,但是运行会出错
}

编译器默认构造函数

除了我们自定义的构造函数外,编译器还会自动生成一些构造函数。当一个类没有定义任何构造函数,且所有成员都有无参构造函数时,编译器会自动生成一个无参构造函数 Pig(),他会调用每个成员的无参构造函数。

但是请注意,这些类型不会被初始化 0

  1. intfloatdouble等基础类型;

  2. voidObject* 等指针类型;

  3. 完全由这些类型组成的类。

这些类型被称为 POD(plain-old-data)。POD 的存在是出于兼容性和性能的考虑。

struct Animal {};

class Pig : public Animal {
public:
	// default construct is implicit defined
	struct Cell { int m_data; } m_c1, m_c2;
	int m_i;
};

以 C 语言的视角来看一下:

struct AnimalInC {};

struct PigInC : public AnimalInC {
	struct Cell { int m_data; }
	Cell m_c1;
	Cell m_c2;
	int m_i;
};

void PigInC_Construct(PigInC* this_) {
	// call animal class constructor
	AnimalInC_Construct(this_);
	// setting vtable ptr if need
	// call member constructor by defined order
	CellInC_Construct(&this_->m_c1);
	CellInC_Construct(&this_->m_c2);
	// m_i is trivial type, leave it uninitialized
}

取决于内存的随机值。

不过,我们可以手动指定初始化 weight 为0,通过 {} 语法指定的初始化值,会在编译器自动生成的构造函数里执行。

类成员的 {} 中还可以有多个参数,甚至能用 =。除了不能用 () 之外,和函数局部变量的定义基本等价。

顺便一提:

int x{};
void *p{};
// 等价
int x{0};
void *p{nullptr};

都会零初始化,但是你不写那个空括号就会变成内存中随机的值。

std::cout << int{};    // 0

初始化列表

当一个类(和它的基类)没有定义任何构造函数,这时编译器会自动生成一个参数个数和成员一样的构造函数。它会将 {} 的内容,按顺序赋值给对象的每一个成员。目的就是为了方便程序员不必手写冗长的构造函数,去一个一个赋值给成员。不过初始化列表的构造函数只支持通过 {}={} 来构造,不支持通过 () 构造。其实是为了向下兼容 C++98。

这个编译器自动生成的初始化列表构造函数,除了可以指定全部成员来构造以外,还可以指定部分的成员,剩余没指定的保持默认。

不过你得保证那个没指定的,有在类成员定义里写明 {} 初始化,否则有可能会变成内存里的随机值。

补充说明:

C++20 中还可以通过指定名称来跳顺序:

Pig pig{.m_weight = 80};

解决函数多返回值

处理函数的复杂类型参数

有自定义构造函数时仍想用默认构造函数:=default

如果还想让编译器自动生成默认的无参构造函数,可以用 C++ 11 新增的这个语法。

不过,初始化列表的那个构造函数没有办法通过 =default 语法恢复。

拷贝构造函数

默认拷贝构造函数

默认的拷贝构造函数有两种形式:

  • Bitwise

  • Memberwise

struct AnimalInC {};

class PigInC : public AnimalInC {
public:
	struct Cell { int m_data; } m_cell;
	int m_i;
};

// bitwise
void PigInC_Copy(PigInC* this_, PigInC* src) {
	memcpy(this_, src, sizeof(*this_);
}

// memberwise
void PigInC_Copy(PigInC* this_, PigInC* src) {
	// call animal class copy constructor
	AnimalInC_Copy((AnimalInC*)this_, (AnimalInC*)src);
	// setting vtable ptr if need
	// call member function by defined order
	CellInC_Copy(&this_->m_cell, &src->m_cell);
	// rough annotation
	this_->m_i = src->m_i;
}

举例:

#include <string.h>
#include <iostream>

struct Pig {
	short m_i;		// 2 bytes padding
	int m_j;

	struct Animal { Animal() = default; Animal(const Animal&) = default; } m_h;
};

int main() {
	Pig a;
	memset(&a, 0xff, sizeof(a));
	Pig b = a;

	std::cout << "a: " << std::hex << *reinterpret_cast<long long*>(&a) << std::endl;
	std::cout << "b: " << std::hex << *reinterpret_cast<long long*>(&b) << std::endl;
}

/*
* a: ffffffffffffffff
* b: ffffffffffffffff
*/
#include <string.h>
#include <iostream>

struct Pig {
	short m_i;		// 2 bytes padding
	int m_j;

	struct Animal { Animal() = default; Animal(const Animal&) { } } m_h;
};

int main() {
	Pig a;
	memset(&a, 0xff, sizeof(a));
	Pig b = a;

	std::cout << "a: " << std::hex << *reinterpret_cast<long long*>(&a) << std::endl;
	std::cout << "b: " << std::hex << *reinterpret_cast<long long*>(&b) << std::endl;
}

/*
* a: ffffffffffffffff
* b: ffffffffccccffff
*/

如果想要使用 Bitwise,数据成员必须是 POD 类型。

POD(Plain Old Data) = Standard Layout + TrivialCopyable

Standard Layout = Compatible With C Memory Layout

https://www.cnblogs.com/ider/archive/2011/07/30/cpp_cast_operator_part3.html

https://www.cnblogs.com/QG-whz/p/4509710.html

https://www.bilibili.com/video/BV1LY411H7Gg/?spm_id_from=333.999.0.0