说明:本文面向有经验的 C++ 程序员,不适合初学者。node
此为《你可能不知道的 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 仍是有点不同。less
C 的结构(struct)不是一种类型,使用时得带着关键字struct
,通常用typedef
来避免这种不便。函数
C++ 的结构几乎等价于类,只是缺省的访问权限为public
而非private
。ui
C++ 的联合(union)能够有成员函数,甚至能够有构造和析构函数。spa
对 C 来讲,一个不带参数的函数意味着能够接受任意参数。因此void f()
就至关于void f(...)
,而下面三个函数指针类型中:ssr
typedef void (*foox)(); typedef void (*foo1)(int); typedef void (*foo2)(void);
foo1
和foo2
能够隐式地转型为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)));
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++ 的数据模型标记(Data Model Notation)。
大写字母表明数据类型(I: int; L: long; LL: long long; P: 指针
),而下标则表示这个类型的大小(通常为 16/32/64)。
有了这个标记,就能够方便地表示不一样平台上 C/C++ 的数据模型。好比 32 位
x86 Linux 平台,数据模型为I32L32LL64P32
,或者简写成IL32LL64P32
。
标准库里处处都是size_t
的身影:
void *malloc(size_t n); void *memcpy(void *s1, void const *s2, size_t n); size_t strlen(char const *s);
回到前面的问题,不难理解如下几点:
size_t
是sizeof
返回值的类型
size_t
是一个typedef
sizeof
不是一个函数,它是一个编译时操做符
size_t
可以表示任何类型理论上可能的数组的最大大小
其实,size_t
通常就是unsigned int
的typedef
,那为何不直接用unsigned int
?在IP16
或IP32
平台上(即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++ 来讲,除了数组外,知足如下条件的类(或结构)也是聚合:
没有用户声明的构造函数
没有private
或protected
非静态数据成员
没有基类
没有虚函数
因此,下面几个类型都是聚合:
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;
n1
和n2
的差异在于,n1
所指的对象是通过零值初始化的,而n2
所指的对象则不肯定。具体来讲,n1
为{ 0, NULL }
,而n2
的value
和next
是什么就说不许了。
成员名字后面加()
就缺省初始化了这个成员。
对于聚合成员来讲,缺省初始化就是指零值初始化。
若是成员有一个 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() {}
由于s
和d
这两个成员都是聚合对象,使用()
就能够初始化为零值。
只能手动赋值了:
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
建立一个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; }
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
在一块预先分配好的内存上建立对象,通常用来实现内存池。好比:
先分配容得下 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 指“资源获取即初始化”(Resource Acquisition Is Initialization),概念上不是很好理解。
也不是全部语言都能支持 RAII。
有些语言,能够把自定义(user-defined)类型的对象分配在栈上(C/C++ 的术语叫“自动的”对象),而且于正常的栈清理时(要么是函数返回,要么是异常抛出)也能一并清理对象,那么它就支持 RAII。典型的语言如 C++。
有些语言,有基于引用计数的垃圾收集,也所以对于只有一个引用的对象具有可预测的清理时,那么它也是支持 RAII 的。典型的语言如 Python。
RAII 的类,设计上都比较纯粹,或者至少主要是用来提供 RAII 的语意。这些类通常都是为某种资源提供一种抽象级别的访问。
C++ STL 中有很多 RAII 类,好比颇具争议性的std::auto_ptr
。还有 std::basic_ifstream
,std::basic_ofstream
,std::basic_fstream
,等等。
C 没有 RAII 这东西,C 也没有自定义类型一说(C 的结构不是一种独立的类型)。
以文件操做为例,C 的作法是:
{ FILE* file = fopen( ... ); // ... fclose(file); }
而 C++ 就方便不少,不须要手动关闭文件,由于std::ofstream
在析构时会自动释放文件资源,即便中途有异常发生也不会出现问题。
{ std::ofstream file( ... ); // 继续操做文件 ... } // 在此 file 对象被自动清理,它的析构函数负责释放文件资源。
Stack Winding & Unwinding
当程序运行时,每个函数(包括数据、寄存器、程序计数器,等等)在调用时,都被映射到栈上。这就是 stack winding。
Unwinding 是以相反顺序把函数从栈上移除的过程。
正常的 stack unwinding 发生在函数返回时;不正常的状况,好比引起异常,调用setjmp
和longjmp
,也会致使 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 工做的原理,它是由语言和编译器来保证的。
第一部分完。