你可能不知道的 C++(一)

说明:本文面向有经验的 C++ 程序员,不适合初学者。node

第二部分:你可能不知道的 C++(二)程序员

此为《你可能不知道的 C++》的第一部分,讨论 C & C++,编译单元,及对象。express


C++ has indeed become too "expert friendly".
C++ 着实已经变得太“面向专家”了。segmentfault

Bjarne Stroustrup, The Problem with Programming, Technology Review, Nov 2006.数组

C++ & C

C++ 是 C 的超集,可是 C++ 中的子集 C 跟原始的 C 仍是有点不同。less

结构 & 联合

  • C 的结构(struct)不是一种类型,使用时得带着关键字struct,通常用typedef来避免这种不便。函数

  • C++ 的结构几乎等价于类,只是缺省的访问权限为public而非privateui

  • C++ 的联合(union)能够有成员函数,甚至能够有构造和析构函数。spa

不带参数的函数

对 C 来讲,一个不带参数的函数意味着能够接受任意参数。因此void f()就至关于void f(...),而下面三个函数指针类型中:ssr

typedef void (*foox)();
typedef void (*foo1)(int);
typedef void (*foo2)(void);

foo1foo2能够隐式地转型为foox,就比如能够从int*char*隐式地转型成void*

要想让一个 C 函数真正没有参数,得用void

void foo(void);

对 C++ 来讲,一个不带参数的函数就是指不接受参数。往参数列表里放void是多余的。

提高void*

详见:Brian W. Kernighan & Rob Pike, The Practice Of Programming, 2.6

C 会自动提高(promote)void*

int* pi = malloc(sizeof(int));

函数malloc返回void*,赋值给int*时不须要显式转型。而 C++ 必须显式转型:

int* pi = static_cast<int*>(malloc(sizeof(int)));

CONSTS

C++ 容许 consts 用在常量表达式中:

const int MAX = 4;
int a[MAX + 1];

switch (i) {
case MAX:
    ...

而 C 则必须使用宏:

#define MAX 4

引一段《C++ 的设计和演化》的原文:
(Bjarne Stroustrup, The Design and Evolution Of C++, 3.8)

In C, consts may not be used in constant expressions. This makes consts far less useful in C than in C++ and leaves C dependent on the preprocessror while C++ programmers can use properly typed and scoped consts.

C 的 consts(特指用 const 关键字修饰的常量)不能够用在常量表达式中。这让 C 的 consts 远不如 C++ 的有用,也让 C 依赖于预处理器,而 C++ 程序员则可使用有适当类型和做用域的 consts。

前置声明

C 代码块中,全部声明必须出如今任何程序语句以前,好比函数定义时,先声明全部局部变量:

void foo() {
    int ival, *p;

    /* … */
}

而 C++ 的声明,诸如int ival;,其自身就是一个程序语句,也所以能够出如今程序文本中的任何位置。

编译单元

C/C++ 中的一个源文件(.c, .cpp, .cc)就是一个编译单元(compilation unit)。
头文件(.h, .hpp)不是编译单元,是不能单独编译的。

源文件通过预处理,先搞定下面这些东西:

  • 宏:包括用户定义的,和预约义的(__cplusplus, __FILE__, ...)

  • 包含语句:源文件中的include语句所有展开

  • 条件编译: #if, #else, #ifudef, ...

  • #error, #warning, ...

预处理过的源文件,通过编译,生成对象文件(.o, .obj)。对象文件通过连接或打包,生成可执行文件或程序库。虽然这里的步骤不太严格,可是大抵就是这样。

若是你对预处理的结果很感兴趣,能够试试编译器的预处理命令:gcc -E (GCC)cl /E or /P (VC)

对象

这里所说的对象(object),泛指一切类型的实例,不仅是类的实例。

关于对象,咱们将探讨如下几个方面:

  • 对象的大小(size)

  • 按存储(storage)分类的对象

  • 聚合(aggregate)

  • 自由存储(free-store)对象的构造和析构

  • RAII

对象的大小

先来考虑几个问题:

  • sizeof是一个函数吗?

  • 你知道sizeof(int), sizeof(long)各为多少吗?

  • 为何应该用size_t

C/C++ 的数据模型

为了回答这些问题,有必要了解一下 C/C++ 的数据模型标记(Data Model Notation)。

C Data Model Notation

大写字母表明数据类型(I: int; L: long; LL: long long; P: 指针),而下标则表示这个类型的大小(通常为 16/32/64)。

有了这个标记,就能够方便地表示不一样平台上 C/C++ 的数据模型。好比 32 位
x86 Linux 平台,数据模型为I32L32LL64P32,或者简写成IL32LL64P32

size_t

标准库里处处都是size_t的身影:

void *malloc(size_t n);
void *memcpy(void *s1, void const *s2, size_t n);
size_t strlen(char const *s);

回到前面的问题,不难理解如下几点:

  • size_tsizeof返回值的类型

  • size_t是一个typedef

  • sizeof不是一个函数,它是一个编译时操做符

  • size_t可以表示任何类型理论上可能的数组的最大大小

其实,size_t通常就是unsigned inttypedef,那为何不直接用unsigned int?在IP16IP32平台上(即int和指针大小一致时),确实没有问题,但I16LP32就不行了。此外,直接用unsigned long当然没错,但毕竟得多花了几个字节,稍微有点浪费了。反正只要用size_t,你就能够同时获得正确性和可移植性。

数据对齐

请问mixed_data的大小是多少?是 8 吗?

struct mixed_data {
    char    data1;
    short   data2;
    int     data3;
    char    data4;
};

在 32 位 x86 平台上编译后的样子:

struct mixed_data {
    char    data1;
    char    padding1[1];
    short   data2;
    int     data3;
    char    data4;
    char    padding2[3];
};

为了数据对齐,编译器塞了一些边角料进去,最终的大小为 12。

按存储分类的对象

C/C++ 的对象,按存储类型分为如下几种:

  • 自动的(auto, register)

  • 静态的(static)

  • 自由存储的(free-store)

关键字auto有点多余,下面两条声明语句其实等价,b前面的auto加不加一个效果:

{
  int a;
  auto int b;
}

到了 C++11,auto这个关键字就被拿来另做他用了:auto可让编译器从变量的初始化上自动推断出它的类型:

auto a = std::max(1.0, 4.0); // 编译器推断出 a 的类型为 double

聚合

首先,什么叫聚合?

对 C 来讲,数组和结构是聚合。

对 C++ 来讲,除了数组外,知足如下条件的类(或结构)也是聚合:

  • 没有用户声明的构造函数

  • 没有privateprotected非静态数据成员

  • 没有基类

  • 没有虚函数

因此,下面几个类型都是聚合:

int[5];

struct person {
    std::string name;
    int age;
};

boost::array;

初始化自动的聚合对象

自动聚合对象能够用“初始化列表”,即花括号,这种初始化方式很是方便。

typedef int ints_t[5];

ints_t ints1 = {};
// { 0, 0, 0, 0, 0 }
ints_t ints2 = { 0, 1, 2 };
// { 0, 1, 2, 0, 0 }
ints_t ints3 = { 0, 1, 2, 3, 4 };
// { 0, 1, 2, 3, 4 }
struct person {
    std::string name;
    int age;
};

person p1 = {};
// { "",  0 }
person p2 = { "john" };
// { "john", 0 }
person p3 = { "john", 26 };
// { "john", 26 }
boost::array<int, 3> a = { 0, 1, 2 };

对聚合对象来讲,缺省初始化(default-initalization)就是指零值初始化(zero-initialization)。

若是不指定初始化列表的话,对象各元素的初始值就不肯定了。

boost::array<int, 3> a; // 三个元素的初始值不肯定

初始化自由存储的聚合对象

初始化列表对自由的聚合对象并不适用,可是自由的聚合对象能够经过()来作零值初始化。

struct list_node {
    int value;
    list_node* next;
};

list_node* n1 = new list_node();
list_node* n2 = new list_node;

n1n2的差异在于,n1所指的对象是通过零值初始化的,而n2所指的对象则不肯定。具体来讲,n1{ 0, NULL },而n2valuenext是什么就说不许了。

成员聚合对象的零值初始化

  • 成员名字后面加()就缺省初始化了这个成员。

  • 对于聚合成员来讲,缺省初始化就是指零值初始化。

  • 若是成员有一个 non-trivial 的构造函数,那么缺省初始化就意味着调用它的缺省构造函数。

如何实现下面这个类的缺省构造函数?

class C {
public:
    C(); // 如何实现这个?
private:
    struct S {
        int x;
        int* p;
        bool b;
    };
    S s;
    int d[5];
};

由 C 转过来的 C++ 程序员可能会这样实现:

C::C() {
    memset(&s, 0, sizeof(S));
    d[0] = d[1] = d[2] = d[3] = d[4] = 0;
}

没错,百分之百正确!可是,更简单更 C++ 的方式是:

C::C() : s(), d() {}

由于sd这两个成员都是聚合对象,使用()就能够初始化为零值。

成员聚合对象的非零值初始化

只能手动赋值了:

C::C() {
    s.x = 5;
    s.p = new char[s.x];
    s.b = true;
    d[0] = 9; 
    ...
}

C++11 新标准容许声明时使用初始化列表:

class C {
    S s { 5, new char[5], true }
    int d[5] { 9, 9, 9, 9 }
};

虽然成员变量的初始化变得简单直观了,可是在头文件里干这种事也有暴露实现细节的嫌疑。

自由存储对象的构造和析构

new operator & operator new

new operator建立一个std::string对象:

std::string* name = new std::string("Adam");

new operator的实现大体以下:

void* p = ::operator new(sizeof(std::string));
std::string* name = static_cast<std::string*>(p);
name->basic_string::basic_string("Adam");

先用operator new分配内存,再转型成std::string指针,而后再调用std::string的构造函数。

负责分配内存的operator new的实现大体以下:

void* operator new(...) {
    void* p;
    while ((p = malloc(size)) == 0) {
        // 尝试调用 new handler 来获得更多可用内存,要不就返回 NULL 或抛出异常
    }
    return p;
}

nothrow & throw

new operator分配内存失败后,缺省的行为不是返回NULL,而是抛出异常std::bad_alloc。因此判断返回值是否为NULL没有任何意义。

A* a = new A();
if (a == NULL) {  // 没有任何意义!
    return;
}

new后面加上(std::nothrow)能够改变这一行为。

char* p = new (std::nothrow) char[0x7ffffffe];
if (p == NULL) {  // 如今能够这样判断了
    // 内存分配失败了!
}

不然就得用 try / catch

char* p;
try {
    p = new char[0x7ffffffe];
} catch (std::bad_alloc&) {
    // 哦,内存分配失败了!
    // ...
}

实践中通常没必要这么麻烦,能够假定new operator总能成功。毕竟连通常对象的内存都分配不了时,程序也没有继续执行的意义了。

placement new

placement new在一块预先分配好的内存上建立对象,通常用来实现内存池。好比:
先分配容得下 3 个std::string的内存:

const size_t count = 3;
char* buf = new char[sizeof(std::string) * count];

而后声明三个std::string对象指针,并逐一调用placement new进行初始化:

std::string* strs[count];
for (size_t i = 0; i < count; ++i) {
    strs[i] = new (buf + i * sizeof(std::string)) std::string("A");
}

最后清理的时候,不能用delete,要手动调用std::string的析构函数~basic_string()来充值对象状态,而内存还留在buf里。要释放buf就单独对它调用delete[]

for (size_t i = 0; i < count; ++i) {
    strs[i]->~basic_string();
}

RAII

RAII 指“资源获取即初始化”(Resource Acquisition Is Initialization),概念上不是很好理解。

也不是全部语言都能支持 RAII。

有些语言,能够把自定义(user-defined)类型的对象分配在栈上(C/C++ 的术语叫“自动的”对象),而且于正常的栈清理时(要么是函数返回,要么是异常抛出)也能一并清理对象,那么它就支持 RAII。典型的语言如 C++。

有些语言,有基于引用计数的垃圾收集,也所以对于只有一个引用的对象具有可预测的清理时,那么它也是支持 RAII 的。典型的语言如 Python。

RAII 的类,设计上都比较纯粹,或者至少主要是用来提供 RAII 的语意。这些类通常都是为某种资源提供一种抽象级别的访问。

C++ STL 中有很多 RAII 类,好比颇具争议性的std::auto_ptr。还有 std::basic_ifstreamstd::basic_ofstreamstd::basic_fstream,等等。

C 没有 RAII 这东西,C 也没有自定义类型一说(C 的结构不是一种独立的类型)。

以文件操做为例,C 的作法是:

{
    FILE* file = fopen( ... );
    // ... 
    fclose(file);
}

而 C++ 就方便不少,不须要手动关闭文件,由于std::ofstream在析构时会自动释放文件资源,即便中途有异常发生也不会出现问题。

{
    std::ofstream file( ... );
    // 继续操做文件 ...
} // 在此 file 对象被自动清理,它的析构函数负责释放文件资源。

RAII 的原理

Stack Winding & Unwinding

当程序运行时,每个函数(包括数据、寄存器、程序计数器,等等)在调用时,都被映射到栈上。这就是 stack winding。

Unwinding 是以相反顺序把函数从栈上移除的过程。

正常的 stack unwinding 发生在函数返回时;不正常的状况,好比引起异常,调用setjmplongjmp,也会致使 stack unwinding。

关于异常发生时,stack unwinding 的过程,不妨引用一段《C++ 程序设计语言》里的原文(Bjarne Stroustrup, The C++ Programming Language, 14.4):

The process of searching “up through the stack” to find a handler for an exception is commonly called “stack unwinding.” As the call stack is unwound, the destructors for constructed local objects are invoked.

可见 stack unwinding 的过程当中,局部对象的析构函数将逐一被调用。这也就是 RAII 工做的原理,它是由语言和编译器来保证的。


第一部分完。

第二部分:你可能不知道的 C++(二)

相关文章
相关标签/搜索