C/C++ tips1
在学习C++的过程中,发现C++真是一个庞大、复杂、微妙的语言。C++虽然有很多设计问题,但是这些年来大家找到了各种各样的方法来克服这些问题,一些特殊的写法初看可能很奇怪,但实际上这些写法都是经过时间考验的专门用来解决某一个问题的经典方法。如果对C++不是特别熟悉,很容易在C++源码中迷失。
我已经先后多次学习C++,但由于我的工作和学习中大部分时间都在使用java,因此总是学了以后,一段时间没有用到就忘了一些内容。这次我重新学习C++,并将有趣的,尤其是C/C++独有的知识点记录在此,以便查阅。这里记录的内容仅仅是对每个主题的简单介绍,不过对主题有了解的人应该能够从这些简单介绍中回忆起相关内容,更加详细的解释说明已有很多经典著作如《Thinking In C++》和《Effective C++》等给出。
c++的设计哲学
Is cpp the world’s most elegant language?
c是一门小巧、优雅、易于上手的语言。
c++不是一门优雅的语言,而是一门实用的语言。
如果想要让一个编程的初学者对编程丧失信心,c++绝对是最佳备选之一。
c++中充斥着undefined behavior,有着各种各样的编程风格,最近的c++委员会每一次升级语言标准都使得c++变得更加复杂更加难以理解,各种编译器对同一套源码进行编译,甚至有矛盾的编译结果。虽然如此,不过c++毕竟经过了这么多年的考验,它的强大是毋庸置疑的。如果多花些时间学习和熟悉C++,就能够掌握这个强大的工具。
extern关键字的作用
extern关键字用在声明变量时,可以告诉编译器这个变量在运行时会由其它程序定义。
extern关键字用在extern “C” {…}时,可以让编译器以C的方式处理块内代码,并且块内必须是标准的C程序。
c++头文件:尖括号和双引号的区别
使用尖括号引用的头文件只在search path中寻找,也就是系统默认头文件目录和我们通过-L指定的目录。使用双引号引用的头文件先相对于文件所在目录查找,如果找不到再去search path中寻找。
早期不同的操作系统规定了不同的文件命名规范,某些系统甚至规定文件名不能超过8个字符,为了兼容这些不同的操作系统,c++标准规定通过尖括号引用头文件时可以不带文件后缀,编译器应当按照当前系统的标准去查找被引用的文件。
c++标准没有强制要求用尖括号引用的头文件必须不带后缀,这使C程序能够直接被C++编译,因为C程序在引用头文件时都是带.h后缀的。c++标准针对C头文件规定了一种特殊的引用格式,可以用 include <cstdio> 替换 include <stdio.h>。这样,可以较为方便地从引用文件的名字看出被引用的是C程序头文件,同时引用方式和新c++头文件不带后缀的引用方式保持一致。
覆盖标准库函数的实现
编译大体上分为编译和链接两个步骤,编译器首先将源代码编译成目标文件,然后将目标文件同库文件链接以解析目标文件中的未定引用。
连接器在链接库文件时,每发现一个未定引用就去指定的库文件列表中查找被引用的符号。我们可以利用这个特性,在我们自己的库文件中实现同标准库函数同名的函数,并让连接器在查找库文件时优先查询我们自己的库文件,这样链接器就会在我们的库文件中发现标准库函数的实现并直接使用该实现,不会继续去标准库文件中查找标准实现了。
被偷偷链接的标准库
在将目标文件编译成可执行程序时,编译器偷偷地帮我们链接了一些库文件。其中一个库文件是启动模块,正是在该库文件中引用并调用了main函数,所以在缺少main函数时,编译器会抱怨说存在未解析的main引用。当执行可执行程序时,实际上是从启动模块库文件的一个入口处开始执行的。
此外,标准库文件也会被偷偷地链接,从而让我们能够直接引用标准库头文件,而不用指明link标准库文件。
struct与POD
C++中,struct和class几乎是同义词,唯一的区别在于默认的访问控制级别。class甚至可以继承自struct,struct也可以继承自class。
通常使用class来声明用到了面向对象思想的类,而使用struct来声明简单的数据类,简单数据类通常被叫做Plain Old Data,即POD。POD是和C兼容的struct,是指对象足够简单,能够直接将其内存写入文件,并随后还能够直接恢复的对象。
比如:
// 写入文件
struct date *object=malloc(sizeof(struct date));
strcpy(object->day,"Good day");
object->month=6;
object->year=2013;
FILE * file= fopen("output", "wb");
if (file != NULL) {
fwrite(object, sizeof(struct date), 1, file);
fclose(file);
}
// 从文件读出
struct date *object2=malloc(sizeof(struct date));
FILE * file= fopen("output", "rb");
if (file != NULL) {
fread(object2, sizeof(struct date), 1, file);
fclose(file);
}
printf("%s/%d/%d\n",object2->day,object2->month,object2->year);
变长结构体
诸如这样的结构体称为变长结构体,其中X[0]在编译期长度为0,其空间在运行时被动态地确定,这是GNU软件的惯常做法:
struct line {
int length; // 在这个属性中记录contents的长度
char contents[0]; // 可变长数组,由于其长度声明为0,因此在struct中不占空间
};
struct line *thisline = (struct line *)
malloc (sizeof (struct line) + this_length);
thisline->length = this_length;
struct f1 {
int x; int y[];
} f1 = { 1, { 2, 3, 4 } };
struct f2 {
struct f1 f1; int data[3];
} f2 = { { 1 }, { 2, 3, 4 } };
注意在使用这种技巧时,要注意可变长数组一定要出现在struct的末尾。当这样的struct被其他struct包含时,不可以使用这种技巧,因为这时在内层struct末尾之后的是外层struct的其它属性,利用这种技巧将导致外层struct的属性被覆盖。
typedef和typename
typename常常和typedef一同使用,比如
typedef typename traits_type::char_type value_type;
typename的作用有两个,第一是用来替代泛型编程中的class关键字以更好地表达范型的语义;第二是用来告诉编译器某个符号是一个类型而非一个变量。上面的例子中typename就告诉编译器traits_type::char_type是一个类型,而不是traits_type类中的一个变量、方法或者别的什么东西。
运算符重载
运算符重载的规则比较复杂,这里的复杂体现在可重载的运算符的数量多、不同运算符重载需要遵循不同的规则:
c++支持的运算符重载有如下这些:
- 正常的可重载运算符:operator op,op可以取:
- 加减乘除、取余、冥乘:+ - * / % ˆ ++ --
- 与、或、非、取反、逻辑与、逻辑或: & | ~ ! && ||
- 赋值、运算并赋值、大于小于等于: = < > += -= *= /= %= ˆ= &= |= != <= >=
- 移位、移位并赋值:<< >> >>= <<= ==
- 其它特殊运算符:, ->* -> ( ) [ ]
- 类型转换函数:operator Type
- 内存分配函数:operator new, operator new [ ]
- 内存释放函数:operator delete, operator delete [ ]
- 用户定义字面量:operator “”
虽然运算符重载可以提供任意的返回类型,但是为了可读性,不同的运算符重载应该遵循各自的规则:
- 二元运算符应该具有反身性,即a+b和b+a应该调用同样的运算符函数,为此二元运算符应该实现为非成员友元函数。顺带一提,二元运算符的定义很微妙,当定义如+这样的运算符时,具体定义的是二元成员运算符还是一元非成员运算符取决于该定义是否有friend作为修饰。如果这样定义:T operator+(const T& rval);,定义的是类型T的二元加法运算符函数,加法的第一个操作数是this;如果在前面加上一个friend,即friend T operator+(const T& rval);,定义的就是一个非成员的友元函数,这个函数是一元运算符+。
- 赋值运算符总是应当检查自赋值,并在自赋值时什么都不做。
- 指针运算符应该返回一个重载了指针运算符的对象或者一个指针,如果返回了一个重载了指针运算符的对象,当调用指针运算符时,会递归地调用指针运算符,直到最后返回了真正的指针,然后在这个指针上调用指针运算符后面指定的方法。
- 使用运算符重载主要是为了更好的可读性,一些数学相关的类型如矩阵、行列式进行运算符重载可以很好地增加可读性;一些智能指针类也可以通过重载赋值运算符和指针运算符来实现对包装对象的透明访问。
- 虽然可以重载指针成员访问运算符、逗号运算符,但是重载它们不仅很麻烦而且通常会破坏可读性。就算是std::shared_ptr也没有重载指针成员访问运算符->*。
类成员指针和成员指针运算符
在C中,每个函数都有地址,可以用一个指针指向这个函数。
在C++中,同样如此,不过C++引入了对象的概念,一个方法有可能是全局函数、类静态函数或者类成员函数。对于全局函数和类静态函数,只要知道这个函数的地址就可以通过函数指针来访问它;但是对于成员函数来说,光是知道函数地址是不够的,它还需要和一个this指针关联,因此要想调用成员函数指针,需要在调用时指定和它关联的this指针,c++特意定义了两个运算符:.和->两个运算符,这两个运算符叫做Pointer-To-Member operator,也就是成员指针运算符。
假设method是一个成员函数指针,如auto method = T::method,如果t是一个对象,就可以通过t.method()来调用method,且在函数中可以通过this来引用&t;如果pt是一个指针,就可以通过pt->method()来调用method,且在函数中可以通过this来引用pt。
Functor,函数对象
Functor是指重载了operator()的类,这样的类对象能够表现得像是一个函数一样,因此得名functor。functor在使用上和函数指针相似,但functor由于自身是一个类的实例,因此可以保持状态,使用functor,可以实现类似函数式编程的代码风格。
新的C++标准引入了lambda用于实现类似的行为,大多数情况下应该可以直接使用lambda。