C++中的C

 

前言

因为C++是以C为基础的,所以要用C++编程就必须熟悉C的语法。

C语言的学习可以学习K & R C的《C程序设计语言》

创建函数

Q: 函数原型?

A: 标准C/C++有一个特征叫函数原型(function prototyping)。调用函数时,编译器使用原型确保正确传递参数并且正确处理返回值,如果调用函数时程序员出错,编译器就会捕获这个错误。

A: 下面是一个声明函数原型的例子:

int translate(float x, float y, float z);

在函数原型中声明变量时,对于同样形式的变量,不能写成translate(float x, y, z)这种形式,而必须指明每一个参数的类型。在函数声明中,下面的形式是可以接受的:

int translate(float, float, float);

因为在调用函数时,编译器只会检查类型,所以使用标识符只是为了使别人阅读代码时更加清晰。

Q: 空参和可变参数列表?

A: 如果有一个空的参数列表,可以在C++中声明这个函数为func(),它告诉编译器,这里有0个参数。应该意识到这只意味着C++中是空参数列表,在C中它意味着不确定的参数数目,这是C语言中的漏洞,因为在这种情况下不能进行类型检查。

A: 在C/C++中,声明func(void)都意味着空的参数列表。

A: 可变的参数列表(variable argument list)用省略号(...)表示。

A: 如果因为某种原因不想使用函数原型的错误检查功能,可以对固定参数表的函数使用可变参数列表,正因为如此,应该限制对C使用可变参数列表并且在C++中也避免使用。我们将会看到,C++中有更好的选择。

Q: 函数的返回值?

A: C++函数原型必须指明函数的返回值类型,在C中,如果省略返回值,表示默认为整型。

A: 为了表明没有返回值可以使用void关键字,如果这时试图从函数返回一个值会产生错误。

A: 下面有一些完整的函数原型:

int f1(void);   // Returns an int, takes no arguments
int f2();       // Like f1() in C++ but not in Standard C!
float f3(float, int, char, double);  // Returns a float
void f4(void);  // Takes no arguments, returns nothing

A: 在一个函数定义中可以有多个return语句。

示例:return.cpp

A: 注意函数声明不是必须的,因为函数在main()使用它之前定义,所以编译器从函数定义中知道它。

Q: 通过库管理器创建自己的库?

A: 我们可以将自己的函数收集到一个库中。在Linux系统中,库的扩展名叫.so或.a

A: 大多数编程包带有一个库管理器来管理对象模块组,每个库管理器有它自己的命令,但有这样一个共同的想法:如果想创建一个库,那么就建立一个头文件,它包含库中的所有函数原型。把这个头文件放置在预处理器搜索路径中的某处,或者在当前目录中(以便能被#include "myheader.h"中发现),或者在包含路径中(以便能被#include<头文件>发现)。

A: 把建成的库和其他库放置在同一个位置以便链接器能发现它,当使用自己的库时,必须向命令行添加一些东西,让链接器知道为你调用的函数查找库。

执行控制语句

本节覆盖了C++中的执行控制语句,在学习C/C++代码之前,必须熟悉这些语句。

C++使用C的所有执行控制语句,这些语句包括if-else、while、do-while、for、swtich选择语句。C++也允许使用声名狼藉的goto语句,应该尽量避免使用goto语句。

Q: 真和假?

A: 表达式产生布尔值true或false,这只是C++中的关键字,在C中如果一个表达式等于非零则表示true。

Q: if-else语句?

A: if-else语句有两种形式:用else或不用else。
第一种形式:

if (表达式) 
{
    语句; 
}

第二种形式

if (表达式) 
{
    语句;
} 
else    
{
    语句;
}

A: 示例:ifthen.cpp

Q: while语句?

A: while、do-while和for语句是循环控制语句。一个语句重复执行直到控制表达式的计值为假。

A: while循环的形式:

while(表达式) 
{
    语句;
}

循环一开始就对表达式进行计算,并在每次重复执行语句之前再次计算。

A: 示例:guess.cpp

Q: do-while语句?

A: do-while的形式是:

do 
{
    语句;
} while(表达式);         

A: do-while语句与while语句的区别在于,即使表达式第一次计值为假,前面的语句也会至少执行一次。在一般的while语句中,如果条件第一次为假,语句一次也不会执行。

A: 示例:guess2.cpp,如果使用do-while,变量guess不需要初始化为0值,因为在它被检测之前就已被cin语句初始化了。

A: 因为某种原因,大多数程序员更多喜欢值使用while语句而避免使用do-while语句。

Q: for语句?

A: 在第一次循环前,for循环执行初始化,然后它执行条件测试,并在每一次循环结束时执行某种形式的“步进(stepping)”。

A: for循环的形式:

for (initialization; conditional; step) {
    语句;           
}

A: 表达式中的initialization、conditional或step都可能为空。

A: 一旦进入for循环,initialization代码就执行,在每次循环之前,conditional被测试,如果它的计值一开始为false,for语句就不会执行,每一次循环结束时,执行step。

A: for循环通常用于“计数”任务,示例:charlist.cpp,运行结果link

Q: 关键字break和continue?

A: break语句退出循环,不再执行循环中的剩余语句;continue语句停止执行当前的循环,返回到循环的起始处开始新的一轮循环。

A: 一个非常简单的菜单系统,示例:menu.cpp

A: 如果用户在主菜单选择'q',则用个关键字break退出,选择其他,程序则继续运行。

A: 在每一个子菜单选择后,关键字continue用于跳转到while循环的起始处。

A: while(true)语句等价于“永远执行这个循环”,当用户按'q'时,break语句使程序跳出这个循环语句。

Q: switch语句

A: switch语句根据一个整型表达式的值从几段代码中选择执行。它的形式如下:

switch(selector) 
{
    case integral-value1 : statement; break;
    case integral-value2 : statement; break;
    case integral-value3 : statement; break;
    case integral-value4 : statement; break;
    case integral-value5 : statement; break;
    (...)
    default: statement;
}

A: 选择器(selector)是一个产生整数值的表达式,switch语句把选择器(selector)的结果和每一个整数值(integral-value)比较。如果发现匹配,就执行对应的语句,如果都不匹配,则执行default语句。

A: switch语句是一种清晰的实现多路选择的方式,即对不同的执行路径进行选择,但它需要一个能在编译时期求得整数值的选择器。

A: 例如,如果想使用一个字符串类型的对象作为一个选择器,在switch语句中它是不能用的,对于字符串类型的选择器,必须使用一系列的if语句并比较在条件中的字符串。

A: 上面的菜单程序提供了一个特别好的switch语句,示例:menu.cpp

Q: 使用和滥用goto?

A: 因为关键字goto存在于C中,所以C++中也支持它。使用goto经常被贬斥为一种糟糕的编码方式,大多数情况也确实如此。

A: 示例:gotokeyword.cpp,运行结果link

A: 一个可供选择的方法是设置一个布尔值,在外层for循环对它进行测试,然后利用break从内层for循环跳出。然而,如果我们有几层for语句或while循环,可能会出现困难。

Q: 递归?

A: 递归也是一种控制流程的一种非常有用的编程技巧,凭借递归我们可以在一个函数内部调用该函数。

A: 如果一直调用下去,会导致内存用完,所以一定要有一种确定“达到底点”的条件,这个条件就叫做基值条件。

A: 示例:cats_in_hats.cpp,运行结果link

A: 求解某些具有随意性的复杂问题经常使用递归,因为这时解的具体“大小”不受限制,函数可以一直递归调用,直到问题解决。

A: 更多参考:Java数据结构和算法 - 递归

运算符简介

我们可以把运算符看作是一种特殊的函数,C++的运算符重载正是以这种方式对待运算符的。

一个运算符带一个或更多的参数并产生一个新值,运算符参数和普通的函数调用参数相比形式上不同,但是作用是一样的。

Q: 优先级?

A: 使用括号使优先级更加清晰

Q: 自增和自减?

A: 可能C语言的设计者认为如果程序员的眼睛不必浏览大范围的印刷区域,那样理解一段巧妙的代码可能是比较容易的。其中一个较好的简洁示例是自增和自减运算符,经常使用它们去改变循环变量以控制循环执行的次数。

A: 如果A是一个整数,前缀++A则先执行运算,再产生结果值;后缀A++,则产生当前值,再执行运算。

A: 示例:autoIncrement.cpp,运行结果link

A: 题外话,C++隐含的意思就是“在C上更进一步”。

数据类型简介

数据类型(data type)定义使用存储空间(内存)的方式,通过定义数据类型,告诉编译器怎样创建一片特定存储空间,以及怎样操纵这边存储空间。

数据类型可以是内部的或抽象的。

内建数据类型(built-in data type )是编译器本来就能理解的数据类型,直接与编译器关联。C和C++中的内建数据类型几乎是一样的。

相反,用户定义的数据类型是我们和别的程序员创建的类型,作为一个类。它们一般被称为抽象数据类型。编译器启动时,通过读包含类声明的头文件认识怎么处理抽象数据类型。

Q: 基本内建类型?

A: 标准C的内建类型规范不说明每一个内建类型必须有多少位,规范只规定内建类型必须能存储的最大值和最小值。

A: 系统头文件limits.h和float.h定义了不同的数据类型可能存储的最大值和最小值。

A: C/C++中有4个基本的内建数据类型。
char用于存储字符,使用最小的一个字节存储,尽管它可能占用更大的空间。
int存储整数值,使用最小两个字节的存储空间。
float和double类型存储浮点数,一般使用IEEE的浮点格式。
float使用单精度浮点数,double用于双精度浮点数。

A: 示例:basic.cpp

A: 如果不初始化一个变量,标准会认为没有定义它的内容,通常这意味着它们的内容是垃圾

A: 程序的第二部分同时定义和初始化变量,如果可能,最好在定义时提供初始值。

A: 注意6e-4中指数符号的使用,意思是“6乘以10的负4次幂”。

Q: bool类型与true/false?

A: 在bool类型成为标准C++的一部分之前,每个人都想使用不同的方法产生类似bool类型的行为,这产生了可移植性问题,可能会引入微妙的错误。

A: 标准C++的bool类型由两种由内建的常量true和false表示的状态。true转换为整数1,false转换为整数0。这3个名字bool、true、false都是关键字。

A: 因为有很多现存的代码使用整型int表示一个标志,所以编译器隐式转换int为bool,非零值为true,零值为false。理想情况下,编译器会给我们一个警告,建议纠正这种情况。

A: 指针在必要的时候也自动转换成bool值。

Q: 说明符?

A: 说明符(specifier)用于改变基本内建类型的含义并把它们扩展成一个更大的集合。

A: 有4个说明符:long、short、signed和unsigned。

A: 整数类型的大小等级是:short int、int、long int。一般int必须至少有short int型的大小。

A: 浮点型的大小等级是:float、double和long double。“long float”是不合法的类型,也没有“short float”。

A: signed和unsigned修饰符告诉编译器怎么使用整数类型和字符的符号位。unsigned数不保存符号,因此有一个多余的位可用,所以它能存储比signed数大一倍的正数。signed是默认的。

A: 示例:specify.cpp,运行结果link

A: 要注意,在不同的机器/操作系统/编译器上运行这个程序得到的结果可能不一样。唯一一致的事情是每个不同类型都具有标准中规定的最小值和最大值。

A: 正如示例所示,当用short或long改变int时,关键字int是可选的。

Q: 指针简介(一)?

A: 不管什么时候运行一个程序,都是首先要把它从磁盘装入计算机内存,因此,程序中的所有元素都驻留在内存的某处。

A: 内存一般被布置成一系列连续的内存位置,我们通常把这些位置看作是8bit的一个字节,但实际每一个空间的大小取决于具体机器的结构,一般称为机器的字长(word size)。

A: 每个空间可按它的地址与其他空间区分。为了便于讨论,我们认为所有机器都使用连续地址的字节从0开始,一直到该计算机的内存的上限。

Q: 指针简介(二)?

A: 因为程序运行时驻留内存中,所以程序中的每一个元素都有地址,假设我们从一个简单的程序开始:

#include <iostream>
using namespace std;

int dog, cat, bird, fish;

void f(int pet) {
  cout << "pet id number: " << pet << endl;
}

int main() {
  int i, j, k;
}

A: 程序运行时,每一个元素在内存中都占有一个位置,甚至函数也占用内存。我们将会看到,定义什么样的元素和定义元素的方式通常决定元素在内存中放置的地方。

Q: 指针简介(三)?

A: C/C++有一个运算符告诉我们元素的地址,这就是‘&’运算符。只要在标识符前加上‘&’,就会得到该标识符的地址。

A: 示例:yourpets.cpp,运行结果link

A: (long)是一种类型转换(cast),意思是“不要把它看成原来的类型,而是看作是long类型”。这个类型转换不是必须的,但是如果没有的话,地址是以十六进制的形式打印,所以转换为long类型会增加一些可读性。

A: 这个程序的结果会随计算机、操作系统和各种其他的因素的不同而变化,但我们总会看到一些有趣的现象。全局变量和局部变量存放在不同的区域,当对语言有更多的了解时,就会明白为什么如此。同样,f()出现在它自己的区域,在内存中代码和数据一般是分开存放的。

A: 相继定义的变量在内存中是连续存放的,它们根据各自的数据类型所要求的字节数分隔开,在本示例中,变量cat距离变量dog是4个字节,同理bird和cat也一样。所以在这台机器上,一个int占4个字节。

Q: 指针简介(四)?

A: 那么利用地址能干什么呢?能做的重要的事就是,把地址存放在别的变量中以便以后使用,C/C++有一个专门的存放地址的变量类型,这个变量就叫做指针(pointer)。

A: 定义一个指针时,必须规定它指向的变量类型,可以先给出一个类型名,然后不是立即给出变量的标识符,而是在类型和标识符之间插入一个星号*,这就是说“等一等,它是一个指针”。一个指向int的指针声明如下:

int* pi;  // pi is points to an int variable

A: 把‘*’和类型联系起来似乎很明白且易读,但是事实上可能容易产生错误。如:

int a, b, c;

而对于指针,可能想写成这样

int* pa, pb, pc;

C/C++不允许像这样合乎情理的表达,在上面的声明中,只有pa是一个指针,而pb和pc是一般的int,可以认为*和标识符结合的更紧密一些。因此最好是每一行定义一个指针,这样就清晰一些:

int *pa = NULL;
int *pb = NULL;
int *pc = NULL;

A: // C++编程的一般原则是在定义的同时就进行初始化。如:

int a = 47;
int *pa = &a;

现在已经初始化a和pa,pa存放a的地址。

Q: 指针简介(五)?

A: 一旦有一个初始化了的指针,我们能做的最基本的事就是利用指针来修改它指向的值。要通过指针访问变量,可以使用以前定义指针使用的同样的运算符来间接引用这个指针,如:

*pa = 100;

现在a的值时100而不是47。

Q: 按值传递?

A: 通常,向函数传递参数时,在函数内部生成该参数的一个拷贝,这称为按值传递(pass-value)。

A: 示例:passbyvalue.cpp,运行结果link

A: 在函数f()中,a是一个局部变量(local variable),它只有在调用函数f()期间存在。因为它是一个函数参数,所以调用函数时通过参数传递来初始化a的值。在main()中参数是x,其值为47,所以当调用函数f()时,这个值被拷贝到a中。

A: 当运行这个程序时,最初,x的值时47,调用f()时,在函数调用期间为变量a分配临时空间,拷贝x的值给a来初始化它,后面我们改变a的值并显示它被改变,但是f()调用结束时,分配给a的临时空间消失了,我们看到,在a和x之间的曾经发生过的唯一联系,是在把x的值拷贝到a的时候。

Q: 按地址传递?

A: 当在函数f()内部时,变量x就是外部对象(outside object),显然,改变局部变量并不会影响外部变量,因为它们分别存在存储空间的不同位置,但是如果我们的确向修改外部对象那又该怎么办呢?

A: 在某种意义上,指针就是另一个变量的别名,所以如果我们不是传递一个普通的值而是传递一个指针给函数,实际上就是传递外部对象的别名,使函数能修改外部对象。

A: 示例:passaddress.cpp,运行结果link,现在函数f()把指针作为参数,并且在赋值期间间接引用这个指针,这就使得外部对象x被修改了。

A: 因此,通过给函数传递指针可以允许函数修改外部对象,这是最基本也是最常用的用途。

Q: C++引用?

A: C++增加了另外一种给函数传递地址的途径,这就是按引用传递(pass-by-reference),它也存在于一些其他的编程语言,并不是C++的发明。

A: 我们可以用引用传递参数地址,引用和指针的区别在于,带引用的函数调用比带指针的函数调用在语法构成上更清晰,在某种情形下,使用引用实质上的确只是语法构成上的不同。

A: 示例:passreference.cpp,运行结果link

A: 在函数f()的参数列表中,不用int*来传递指针,而是用int&来传递引用。在f()中,如果仅仅写‘r’,会得到r引用的变量值,如果对r赋值,实际上是给r引用的变量赋值,事实上,得到r中存放的地址值的唯一是用‘&’运算符。

A: 在函数main(),我们能看到引用在调用函数f()中的重要作用,其语法形式还是f(x)。尽管这看起来像是一般的按值传递,但是实际上引用的作用是传递地址,而不是值的一个拷贝。

A: 所以我们可以看到,以引用传递允许一个函数去修改外部对象,就像传递一个指针所做的那样,通过这个简单的示例,我们可以认为引用仅仅是语法上的不同方法(有时称为“语法糖syntactic sugar”)。

Q: 用指针和引用作为修饰符 ?

A: 迄今为止,我们已经看到了基本的数据类型char、int、float和double,看到了修饰符signed、unsigned、short和long,它们可以和基本的数据类型结合使用,现在我们增加了指针和引用,所以可能产生了三倍的组合。

A: 示例:alldefinition.cpp

Q: 万能指针void*(一)?

A: 如果声明指针是void*,它意味着任何类型的地址都可以间接引用那个指针,而如果声明是int*,则只能对int型变量的地址间接引用那个指针。

A: 示例:voidpointer.cpp

Q: 万能指针void*(二)?

A: 一旦我们间接引用一个void*,就会失去关于类型的信息,这意味着在使用前,必须转换为正确的类型。

A: 示例:CastFromVoidPointer.cpp

A: 转换(int*)pv告诉编译器把void*当做int*处理,因此可以成功地对它间接引用。

A: 我们注意到这个语法相当难看,的确如此,但是更糟的是,void*在语言类型系统引入了一个漏洞,也就是说,它允许甚至是提倡把一种类型看作另一种类型。

A: 在上面示例中,通过把pv转换为int*,把它看作一个整型,但是并没有说不能把它转换为一个char*double*,这将改变已经分配给int的存储空间的大小,可能会引起程序崩溃。

A: 一般来说应该避免使用void指针,只有在一些少见的特殊情况才用。

A: 我们不能使用void引用。

作用域

Q: 规则?

A: 作用域规则告诉我们一个变量的有效范围,它在哪里创建,在哪里销毁(即超出了作用域)。

A: 变量的有效作用域从它的定义点开始,到和定义变量之前最邻近的开括号配对的第一个闭括号,也就是说,它的作用域是由变量所在的最近一对括号确定。

A: 示例:scope.cpp

A: 上面的示例表明什么时候变量可见,什么时候变量不可用,只有在变量的作用域内才能使用它。

A: 作用域可以嵌套。其内层可以访问外层,反过来不行。

Q: 实时定义变量?

A: 定义变量时,C和C++有着显著的区别,这两种语言都要求变量使用前必须定义,但是C强制在作用域的开始处就定义所有的变量,以便在编译器创建一个块时,能够给所有的变量分配空间。

A: 读C代码时,进入一个作用域,首先看到的是一个变量的定义块,在块的开始部分声明所有的变量,要求程序员以一种特定的方式写程序,因为语言的实现细节需要这样。大多数人在写代码之前并不知道它们将要使用的所有变量,所以他们必须不停地跳转回块的开头来插入新的变量,这是很不方便的,也很容易引起错误。这些变量定义对读者来说并没有很多含义,它们实际上只是容易引起混乱,因为它们出现的地方远离使用它们的上下文。

A: C++(不是C)允许在作用域内的任意地方定义变量,所以可以在正好使用它之前定义。此外,可以在定义变量时对它进行初始化以防止犯某种类型的错误。以这种方式定义变量使得编写代码更容易,减少了在一个作用域内不停地来回跳转造成的问题。

A: 同时定义并初始化一个变量是非常重要的。

A: 我们还可以在for循环和while循环的控制表达式内定义变量,在if语句的条件表达式和switch的选择器语句内定义变量。

A: 示例:OnTheFly.cpp

A: 尽管例子表明在while语句、if语句和switch语句中也可以定义变量,但是可能因为语法受到许多限制,这种定义不如for的表达式中常用。例如,我们不能有任何插入括号,也就是说,不可以写出:

while((char c = cin.get()) != 'q')

附加的括号似乎是合理的,并且能做很有用的事,但因为无法使用它们,结果就不像所希望的那样。问题是‘!=’比‘=’的优先级高,所以char c最终含有的值是由bool转换为char的,当打印出来时,我们在很多终端上看到一个笑脸字符。

A: 通常,可以认为在while语句、if语句和switch语句中定义变量的能力是为了完备性(completeness),但是唯一使用这种变量定义的地方可能是for循环中,在这里使用的十分频繁。

指定存储空间分配

创建一个变量时,我们拥有指定变量生存期的很多选择,指定怎样给变量分配存储空间,以及指定编译器怎样处理这些变量。

Q: 全局变量?

A: 全局变量时在所有函数体的外部定义,程序的所有部分,甚至其他文件中的代码,都可以使用。

A: 全局变量不受作用域的影响,总是可用的,也就是说,全局变量的生命周期一直到程序的结束。

A: 如果在一个文件中使用extern关键字来声明另一个文件中存在的全局变量,那么这个文件就可以使用这个数据。

A: 示例:global,运行结果link

A: 变量globe的存储空间是由代码global.cpp中定义创建的,在global2.cpp的代码中可以访问同一个变量。由于global2.cpp和global.cpp的代码是分段编译的,必须通过声明:

extern int globe;

告诉编译器变量存在哪里。

A: 运行这个程序时,会看到函数func()的调用的确影响globe的全局实例。

Q: 局部变量(一)?

A: 局部变量出现在一个作用域,它们局限于一个函数内。

A: 局部变量经常被称为自动变量(automatic variable),因为它们在进入作用域时自动生成,离开作用域时自动消失。

A: 关键字auto可以显示地说明这个问题,但是局部变量默认认为是auto,所以没有必要声明为auto。

Q: 局部变量(二)?

A: 寄存器变量(register variable)是一种局部变量。

A: 关键字register告诉编译器“尽可能快地访问这个变量”,加快访问速度取决于实现,但是,正如名字所暗示的那样,这经常是通过在寄存器放置变量来做到的。这并不能保证将变量放置在寄存器中,甚至也不能保证提高访问速度。这只是对编译器的一个暗示。

A: 使用register变量是有限制的,不可能得到或计算register变量的地址。register变量只能在一个块中声明,不可能有全局的或静态的register变量。然而可以在一个函数中使用register变量作为一个形式参数。

A: 一般地,不应当推测编译器的优化器,因为它可能比我们做得更好,因此,最好避免使用关键字register。

Q: Static关键字(一)之静态变量?

A: 通常,函数中定义的局部变量在函数作用域结束时消失,当再次调用该函数时,会重新创建该变量的存储空间,其值会被重新初始化。

A: 如果想使局部变量的值在程序的整个生命周期仍然存在,我们可以定义函数的局部变量为static,并给它一个初始值。

A: 初始化只在函数第一次调用时执行,函数调用之间变量的值保持不变。用这种方式,函数可以“记住”函数调用之间的一些信息片段。

A: 我们可能奇怪为什么不使用全局变量,static变量的优点是在函数范围之外它是不可用的,所以它不可能被轻易地改变。

A: 示例:static.cpp,运行结果link

A: 每一次在for循环中调用函数func()时,它都打印不同的值,如果不使用关键字static,打印出的值总是1。

Q: Static关键字(二)?

A: static的第二层意思和前面的含义相关,即“在某个作用域外不可访问”。

A: 当用static修饰函数名和所有函数外部的变量时,它的意思是“在文件的外部不可以使用这个名字”。函数名或变量是局部于文件的,我们说它具有文件作用域(file scope)。

A: 示例:filestatic,编译和链接下面文件会引起链接器错误:

/tmp/ccGRjHFp.o:在函数‘func()’中:
filestatic2.cpp:(.text+0x6):对‘fs’未义的引用
collect2: error: ld returned 1 exit status

A: 尽管在文件filestatic2.cpp中变量fs被声明为extern,但是链接器不会找到它,因为在filestatic.cpp中它被声明为static。

A: static说明符也可以在一个类中使用,在后面介绍如何创建类时,在对此作出解释。

Q: 外部变量

A: 前面已经简单地描述和说明了extern关键字。它告诉编译器存在着一个变量和函数,即使编译器在当前编译的文件中没看到它,这个变量或函数可能在另一个文件中或者在当前文件的后面定义。

A: 示例:forward.cpp,运行结果link

A: 当编译器遇到extern int i时,它直到i肯定作为全局变量存在于某处。当编译器看到变量i的定义时,并没看到别的声明,所以知道它在文件的前面已经找到了同样声明的i。

Q: 链接?

A: 为了理解C/C++程序的行为,必须对链接(linkage)有所了解。在一个执行程序中,标识符代表存放着变量或被编译过的函数体的存储空间。链接用链接器(linker)所见的方式描述存储空间。

A: 链接方式有两种:内部链接(internal linkage)和外部链接(external linkage)。

A: 内部链接意味着只对正被编译的文件创建存储空间。用内部链接,别的文件可以使用相同的标识符或全局变量,链接器不会发现冲突,也就是为每一个标识符创建单独的存储空间。

A: 在C/C++中内部链接是由关键字static指定。

A: 外部链接意味着为所有被编译过的文件创建一片单独的存储空间。一旦创建存储空间,链接器必须解决所有对这片存储空间的引用。

A: 全局变量和函数名有外部链接,通过用关键字extern声明,可以从其他文件访问这些变量和函数。

A: 函数之外定义的所有变量(在C++除了const)和函数定义默认为外部链接,可以使用关键字static特地强制它们具有内部链接,也可以在定义时使用关键字extern显示指定标识符具有外部链接。

A: 在C中,不必用extern定义变量或函数,但是在C++中对于const有时必须使用。

A: 调用函数时,自动变量(局部变量)只是临时存在于堆栈中,链接器不知道自动变量,所以这些变量没有链接。

Q: 常量(一)?

A: 在旧版本(标准前)的C中,如果想建立一个常量,必须使用预处理器:

#define PI 3.14159

A: 在C和C++中都可以使用这个宏。

A: 当使用预处理器创建常量时,我们在编译器的范围之外能控制这些常量。对名字PI上不进行类型检查,也不能得到PI的地址,所以不能向PI传递一个指针和一个引用。

A: PI不能是用户定义的类型变量。

A: PI的意义是从定义它的地方持续到文件结束的地方,预处理器并不识别作用域。

A: C++引入了常量来代替上面的宏,常量就像变量一样,只是它的值不能改变。修饰符const告诉编译器这个名字表示常量,如果定义了某对象为常量,然后试图修改它,编译器就会报错。

A: 必须用下面的方式说明一个常量类型:

const int X = 10;

Q: 常量(二)?

A: 在标准C和C++中,可以在参数列表中使用常量,即使列表中的参数是指针或引用,也就是说,可以获得从const的地址。const就像正常的变量一样有作用域。

A: const是由C++采用,并加进标准C中。在C中,编译器对待constant如同变量一样,只不过带有一个特殊的标记,意思是“不要改变我”。当在C中定义const时,编译器为它创建存储空间,所以如果在两个不同的文件中或在头文件中定义多个同名的const,链接器将生成刚发生冲突的错误消息。

A: 在C中使用const和在C++中使用const是完全不一样的,简而言之,在C++中使用的更好。

Q: 常量值?

A: 在C++中,一个const必须有初始值,在C中不是这样的。

A: 内建类型的常量值可以表示为十进制、八进制、十六进制、浮点型或字符,不幸的是,二进制被认为是不重要的。

A: 如果没有其他的线索,编译器会认为常量值是十进制。

A: 常量值前带0被认为是八进制。

A: 常量值前带0x被认为是十六进制。

A: 浮点数可以含有小数点和指数幂(用e表示,意思是10的幂),小数点和e都可以任选。如果给一个浮点变量赋一个常量值,编译器会取得这个常量值并把它转换成浮点数,这个过程是隐式类型转换(implicit type conversion),但是,使用小数点或e对于提醒读者当前正在使用的是浮点数是一个好主意。

A: 合法的浮点常量值如:1e4、1.0001、47.0、0.0、-1.159e-77等等。我们可以对数加后缀强加浮点数类型:f或F强加float型,L或l强加long double型,否则是double型。建议不要用小写字母l,因为它看起来很像数字1。

A: 字符常量时用单引号括起来的字符,如'A'、'0'、' '等等。注意'0'和数值0之间存在巨大差别。

A: 用“反斜杠”表示一些特殊的字符,如:'\n'(换行)、'\t'(制表符)、'\\'(反斜杠)、'\r'(回车)、'"'(双引号)、'''(单引号)等等,也可以用八进制表示字符常量,如'\17',或用十六进制表示字符常量,如'xff'。

Q: volatile变量?

A: 限定词const告诉编译器“这是不会改变的”,而限定词volatile则告诉编译器“不知道何时会改变”,防止编译器依据变量的稳定性做任何优化。

A: 当读在代码控制之外的某个值时,例如读一块通信硬件中的寄存器,将使用这个关键字,无论何时需要volatile变量的值,都能读到,即使在该行之前刚刚读过。

A: “在代码的控制之外”的某个存储空间的一个特殊例子是在多线程程序中,如果正在观察被另一个线程修改的特殊标识符,这个标识符应该是volatile,所以编译器不会认为它能够对标识符的多次读入进行优化。

A: 注意当编译器不进行优化是,volatile可能不起作用,但是当开始优化代码时,如当编译器开始寻找冗余的读入时,可以防止出现重大的错误。

A: 后面还会进一步阐述const和volatile关键字。

运算符及其作用

所有的运算符都会从它们的操作数中产生一个值。除了赋值、自增(自减)运算符之外,运算符所产生的值不会修改操作数。

修改操作数被称为副作用(side effect),一般使用修改操作数的运算符就是为了产生这种副作用,但是应该记住它们所产生的值就像没有副作用的运算符产生的值一样都是可以使用的。

Q: 赋值运算符?

A: 赋值操作是由运算符“=”实现,这意味着“取右边的值”并把它拷贝给左边。

A: 右边的值通常称为右值(rvalue),同理也有左值(lvalue)的概念。

A: 右值可以是任意的常量、变量或能产生值的表达式,但是左值必须是一个明确命名的变量,也就是说应该有一个存储数据的物理空间。

A: 例如,可以给一个变量赋值常量:

A = 4;

但是不能给常量赋任何值,因为它不能是左值,不能写成如下:

4 = A;

Q: 数学运算符?

A: 基本的数学运算符: addition(+)、subtraction(-)、multiplication(*)、division(/)、multiplication(%)。

A: 整数相除会截取结果的整数部分,不舍入。

A: 浮点数不能使用取模运算符。
示例:FloatCanNotModulus.cpp,编译结果link

A: C/C++也使用一种简化的符号来同时执行操作和赋值,这是由一个运算符后面跟着一个赋值号来表示。例如:X += 4;

A: 示例:mathops.cpp,运行结果link

A: 注意,使用宏PRINT()可以节省输入和避免输入错误。传统上用大写字母来命名预处理宏以便突出它。后面我们很快会了解宏有可能会变得很危险。

A: 跟在宏后面的括号中的参数会被闭括号后面的所有代码替代。只要在调用宏的地方,预处理程序就删除名字PRINT并替换代码,所以使用宏时编译器不会报告任何错误信息,它并不对参数做任何类型检查。

Q: 关系运算符?

A: 关系运算符在操作数之间建立一种关系。如果关系为真,则产生布尔值true;如果关系为假,则产生布尔值false。

A: 关系运算符有:<、>、<=、>=、==、!=

A: 参考:C/C++ 浮点数比较问题

Q: 逻辑运算符?

A: &&(逻辑与)、||(逻辑或)

A: 记住在C/C++中,如果语句是非零值则表示true,为零则为false

A: 示例:boolean.cpp

A: 我们可以用float或double代替int的定义,但是注意浮点数和零比较时很严格的,一个数和另一个数即使只有最小小数位不同仍然是“不相等”。

Q: 位运算符?

A: 因为浮点数使用一种特殊的内部格式,所以位运算符只适用于整型char、int和long。

A: 位运算符包括:&(位与运算符)、|(位或运算符)、^(位异或运算符xor)、~(非运算符,也称补运算符)。

A: ~运算符是一个一元运算符,它只带一个参数。

A: 位运算符可以和“=”结合来统一运算和赋值,如:&=、|=、^=都是合法运算。

A: 因为~是一元运算符,所以不能和=结合。

Q: 移位运算符(一)?

A: 左移位运算符(<<)引起运算符左边的操作数向左移动,移动的位数由运算符后面的操作数指定。

A: 右移位运算符(>>)引起运算符左边的操作数向右移动,移动的位数由运算符后面的操作数指定。

A: 如果移位运算符后面的值比运算符左边的操作数的位数大,则结果是不定的。

A: 如果左边的操作数是无符号的,右移是逻辑移位,所以最高位补零。

A: 如果左边的操作数是有符号的,右移可能是也可能不是逻辑移位,行为是不确定的。

A: 移位可以和赋值号结合,<<=>>=,左值由左值按右值移位后的结果代替。

A: 示例:bitwise.cpp,运行结果link

A: 在main()中,变量都是unsigned的,这是因为一般来说,在使用字节进行工作时并不希望用带符号数。

A: 对于变量getval而言,可能要使用int来代替char,因为语句cin >> getval以另一种方式把第一个数字看成是一个字符,通过把getval赋值给a和b,该值被转换为一个单独的字节。

Q: 移位运算符(二)?

A: 当移位越出数的一端时,那些位就会丢失,那些位掉进了神秘的位桶(bit bucket)里,丢弃在这个桶中的位有可能需要重用。

A: 操作位时,也可以执行旋转(rotation),即在一端移掉的位插入到另一个端,好像它们在绕着一个回路旋转。尽管大多数计算机处理器提供了机器级的旋转命令,但是在C/C++中,不直接支持旋转。

A: 示例:bitwise.cpp,运行结果link

Q: 一元运算符?

A: 唯一使用一个参数的运算符

A: 位的非运算(~)

A: 逻辑非(!)

A: 一元减(-)和一元加(+),例如,语句x = -a;

对于这种x = a * -b,编译器可以理解,但是读者可能迷惑,所以写成x = a * (-b)。

一元减得到一个负值,一元加实际上不做任何事,只是和一元减相对应。

A: 自增(++)和自减(--)运算符,它们是涉及赋值的运算符中仅有的副作用的运算符

A: 取地址运算符(&)

A: 间接引用(*或->)

A: 强制类型转换运算符

A: C++的new/delete

Q: 三元运算符?

A: 第一个表达式 ? 第二个表达式 : 第三个表达式

A: 例如: a= --b ? b : (b = -99);
条件产生右值,如果b自减运算的结果为非零,则把b赋值给a;如果b变为零,a和b都被赋值-99。

A: 示例:TernaryOperator.cpp

Q: 逗号运算符?

A: 逗号并不只是在定义多个变量时用来分隔变量,例如:int i, j, k;

A: 当然它也用于函数参数列表中。

A: 然而,它也可能作为一个运算符用于分隔表达式,在这种情况下,它只产生最后一个表达式的值,在逗号分隔的列表中,其余的表达式的计算完成它们的副作用。

A: 示例:CommaOperator.cpp,运行结果link

A: 通常除了作为一个分隔符,逗号最好不做他用,因为人们不习惯把它看作是运算符。

Q: 转换运算符(一)?

A: 转换(cast)这个词的含义是“浇铸成一个模型(casting into a mold)”。

A: 例如,如果赋给一个整型值给一个浮点变量,编译器会暗地里调用一个函数,或更可能插入代码来把整型转换为浮点型。

A: 转换允许将此类型转换为显式(conversion explicit),或在转换没有正常情况下发生时,强制使用转换运算符来实现。

A: 转换运算符是用括号把所想要转换的数据类型(包括所有的修饰符)括起来放在值的左边,这个值可以是一个变量、一个常量、一个表达式产生的值、一个函数的返回值等。

A: 示例:simplecast.cpp

A: 转换是很有用的,但是它也造成了令人头痛的事情,因为在某些情况下,它强制编译器把一个数据看作是比它实际上更大的类型,所以它占用了更多的内存空间,这可能会踩踏(trample)内存中的其他数据

A: 这种情况经常不是在上述简单的类型转换示例中发生,而是在转换指针时经常发生。

Q: 转换运算符(二)?

A: C/C++还有另外一种转换语法,它遵从函数调用的语法格式,就是直接给参数加上括号而不是给数据类型加上括号。

A: 示例:FuncCallCast.cpp

A: 当然对于示例中的代码,我们实际上不需要转换,只要写200f即可。转换一般用于变量,而不用于常量。

Q: C++的显示转换(一)?

A: 应该小心使用转换,因为转换实际上要做的就是对编译器说,“忘记类型检查,把它看作是其他类型”,也就是说,在C++类型系统中引入了一个漏洞,并阻止编译器报告在类型方面出错了。更为糟糕的是,编译器会相信它,而不执行任何其他的检查来捕获这种错误。

A: 一旦开始进行转换,程序员必须自己面对各种问题。实际上,无论什么原因,任何一个程序如果使用很多转换都值得怀疑。一般情况下,很少使用转换,它只是用于解决非常特殊的问题。

A: 一旦理解这一点,在碰到一个发生故障的程序时,第一个反应应该是寻找转换这个可疑点,但是怎么确定C风格转换的位置呢?它们只是在括号中的类型名字,如果开始查找这些代码的话,会发现很难把它们和其他部分的代码区分开来。

A: 标准C++提供了一个现实的转换语法,来完全替代旧的C风格转换,当然,如果不破坏代码,是不会认为C风格的转换不合法。使用显示类型转换语法使我们很容易发现它们,因为通过它们的名字就能找到:

关键字说明
static_cast For “well-behaved” and “reasonably well-behaved” casts, including things you might now do without a cast (such as an automatic type conversion). 
用于“良性”和“适度良性”转换,包括不用强制转换(例如自动类型转换)
const_cast To cast away const and/or volatile. 
对“const”和/或“volatile”进行转换
reinterpret_cast To cast to a completely different meaning. The key is that you’ll need to cast back to the original type to use it safely. The type you cast to is typically used only for bit twiddling or some other mysterious purpose. This is the most dangerous of all the casts. 
转换为完全不同的意思。为了安全使用它,关键必须转换为原来的类型。转换成的类型一般只能用于位操作,否则就是为了其他隐秘的目的。这就是所有转换中最危险的。
dynamic_cast For type-safe downcasting. 
用于类型安全的向下转换

Q: 静态转换(static_cast)?

A: static_cast全部用于明确定义的转换,包括编译器允许我们所做的不用强制转换的“安全转换”和不太安全但清楚含义的转换。

A: static_cast包括的转换类型有:典型的非强制转换(typical castless conversions)、窄化变换(narrowing conversions)、使用void*的强制转换、隐式类型转换(implicit type conversions)、类层次的静态定位(static navigation of class hierarchies)。

A: 示例:StaticCast.cpp

A: 程序的第(1)部分,是C中习惯采用的几种转换,有的有强制转换,有的没有强制转换,把int提升到long或者float是不会有问题,因为后者总能容纳一个int的值,尽管这是不必要的,但是可以使用static_cast来凸显这个提升(Promot)转换。

A: 第(2)部分显示的是另一种转换,在这里可能会丢失数据,因为一个int和long/float不是一样“宽”的,它不能容纳同样大小的数字,因此成为窄化转换。编译器仍能执行这种转换,但是会经常给出一个警告。我们可以消除这种警告,表明我们真的想使用转换来实现它。

A: 第(3)部分中,C++中不用转换是不允许从void*赋值的,不像C,这是很危险的,要求程序员知道他们正在做什么。至少,当查找故障时,static_cast比旧标准的转换更容易定位。
编译报错的信息如下:

error: invalid conversion from ‘void*’ to ‘float*’ [-fpermissive]

A: 第(4)部分显示编译器自动执行的几种隐式类型转换,这些转换是自动的,不需要强制转换,但是当我们要想清楚发生了什么或者以后要查找转换,可以再次使用static_cast突出这个行为。

Q: 常量转换(const_cast)?

A: 从const转换非const,或从volatile转换为非volatile,可以使用const_cast。这是const_cast唯一允许的转换。如果进行别的转换就可能要使用单独的表达式或者可能会得到一个编译错误:

A: 示例:ConstCast.cpp

A: 如果取到了const对象的地址,就可以生成一个指向const对象的指针,不用转换是不能将它赋值给非const指针的。旧形式的转换能实现这样的赋值,但const_cast也许更合适。

A: 同理volatile也是一样。

Q: 重解释转换(reinterpret_cast)?

A: 这是最不安全的一种转换机制,最有可能出现问题。

A: A reinterpret_cast pretends that an object is just a bit pattern(位模式) that can be treated (for some dark purpose) as if it were an entirely different type of object。这是低级的位操作,C因此而名声不佳。

A: 在使用reinterpret_cast做任何事之前,实际上总是需要reinterpret_cast回到原来的类型,或者把变量看作是它原来的类型。

A: 示例:ReinterpretCast.cpp

A: struct X只包含一个整型数组,但是当用X x在堆栈中创建一个变量时,该结构体中的每一个整型变量的值时没有意义的,垃圾值,通过使用函数print()把结构体的每一个整型值打印出来可以表明这一点。

A: 为了初始化它们,取得X的地址并转换为一个整型指针,该指针然后遍历这个数组,并置每一个元素为0。注意i的上限是通过计算sz + xp得到,这是指针算术运算。

A: reinterpret_cast的思想就是当需要使用的时候,所得到的东西已经不同了,以至于它不能用于类型的原来目的,除非再次把它转回回来。这里我们在print()调用前中把xp转换为X*

A: 使用reinterpret_cast通常是一种不明智、不方面的编程方式,但是当必须使用它时,它又是非常有用的。

Q: sizeof - 独立运算符(一)?

A: sizeof单独作为一个运算符是因为它满足不同寻常的需要,sizeof给我们提供了对有关数据项目所分配的内存大小,它会告诉我们任何变量使用的字节数,同时它也可以给出数据类型(data type, with no variable name)的大小。

A: 示例:sizeof.cpp

A: By definition, the sizeof any type of char (signed, unsigned or plain) is always one, regardless of whether the underlying storage for a char is actually one byte.

A: 对于所有别的类型,结果都是以字节表示的大小

Q: sizeof - 独立运算符(二)?

A: 注意sizeof是一个运算符,不是函数,如果把它应用于一个类型,必须要像上面示例的那样使用括号。但是如果对一个变量使用它,则可以不要括号。

A: 示例:SizeofOperator.cpp

A: sizeof也可以给出用户定义的数据类型的大小。

Q: asm关键字?

A: 这是一种转义(escape)机制,允许在C++程序中写汇编代码。

A: 在汇编程序代码中经常可以引用C++的变量,这意味着可以方便地和C++代码通信,且限定汇编代码只是用于必要的高效调整,或使用特殊的处理器指令。

创建复合类型

C/C++提供的工具允许把基本的数据类型组合成复杂的数据类型,使用关键字struct,在C++中这是类的基础。但是创建比较复杂的类型的最简单的一种方式,只需要通过typedef来命名一个名字为另一个名字。

Q: 用typedef命名别名?

A: typedef表示“类型定义(type definition)”,但用“别名”来描述可能更加精确。

A: 下面是一种经常使用的typedef:

typedef unsigned long ulong;

A: 现在如果写ulong,则编译器知道意思是unsigned long,我们可能认为使用预处理程序置换可以很容易实现,但是在一些重要的场合,编译器必须知道我们正在将名字当做类型处理,所以typedef起了关键作用。

A: typedef 经常会派上用场的地方是指针类型,如果写成:

int* x, y;

This actually produces an int* which is x and an int (not an int*) which is y. That is, the ‘*’ binds to the right, not the left.

但是如果使用typedef:

typedef int* IntPtr;
IntPtr x, y;

则x和y都是int*类型。

A: 有人可能争辩说避免使用typedef定义基本类型会更清楚,因此更可读,而使用大量typedef时,程序的确变得难以阅读。但是,在C中使用struct时,typedef是特别重要。

Q: 一个简单的结构体?

A: struct(结构)是把一组变量组合成一个构造的一种方式。一旦创建了一个struct,就可以生成所建立的新类型变量的许多实例。

A: 示例:SimpleStruct.cpp

A: struct声明必须以分号结束。

A: 在main()中,创建了两个Structure1的实例:s1和s2,它们每一个都有各自独立的c、i、f和d版本。所以s1和s2表示了完全独立的变量块。

A: 注意,在C中,当定义变量时,必须说struct Structure1,不能只说Struct1,这个就是C使用的不便之处,所以使用typedef就特别方便。在C++中可以直接只写Struct1。
gcc编译SimpleStruct.c文件报错的信息如下:

$ gcc SimpleStruct.c
SimpleStruct.c: In function ‘main’:
SimpleStruct.c:17:5: error: unknown type name ‘Structure1’
     Structure1 s1, s2;
     ^
SimpleStruct.c:19:7: error: request for member ‘c’ in something not a structure or union
     s1.c = 'a';
       ^

Q: 用struct把变量结合在一起(一)?

A: 示例:SimpleStruct.cpp

A: 通过使用typedef,可以假设Structure2是一个像int一样的内建类型,我们将会看到,struct标识符已经脱离了。

Q: 用struct把变量结合在一起(二)?

A: 有时候可能需要早定义结构是使用struct。这时,可以重复struct的名字,就像struct名和typdef一样

A: 示例:SelfReferential.cpp

A: 如果看一下这个程序,会看到sr1和sr2互相指向,且每个都拥有一个块数据。

A: 实际上,struct的名字不必和typedef的名字相同,但是,一般使用相同的名字,为了使得事物更加简单。

Q: 指针和struct?

A: 所有的struct都当做对象处理,为了选择一个特定struct对象中的元素,应当使用.操作符,但是,如果一个指向struct对象的指针p,就得写成(*p).,C/C++提供一个更简单的运算符->来完成这个事情。

A: 示例:SimpleStruct.cpp

A: 在main()中,struct指针sp最初指向s1,用->操作符选择s1中的成员来初始化它们,随后sp指向s2,以同样的方式初始化那些变量。所以可以看到指针的另一个好处是可以动态地重定向它们,指向不同的对象,使编程更灵活。

Q: 用enum提高程序清晰度?

A: 枚举数据类型是把名字和数字相联系的一种方式,从而对阅读代码的任何人给出更多的含义。

A: enum关键字,来自C,通过为所给出的任何标识符表赋值0、1、2等值来自动地列举出它们,也可以声明为enum变量(它们总是表示为整数值)。

A: 示例:enum.cpp

A: shape是被列举的数据类型ShapeType的变量,可以把它的值和列举的值相比较,因为shape实际上只是int,所以它可以具有任何一个int拥有的值,包括负数,也可以把int变量和枚举值比较。

Q: 给枚举内的名字赋值?

A: 如果不喜欢编译器赋值的方式,可以自己做。例如:

enum ShapeType {
    circle = 10, square = 20, rectangle = 50
};

A: 如果对某些名字赋给值,对其他的不赋给值,编译器会使用相邻的下一个整数值。例如:

enum snap { crackle = 25, pop };

编译器会把26赋值给pop。

A: 使用枚举数据类型时,增强了代码的可读性,然后,在某种程度上在C中实现C++中用类可以做到的事,所以在C++中很少看到使用enum。

Q: 枚举类型检查?

A: C的枚举相当简单,只是把整数值和名字联系起来,但它们并不提供类型检查,而在C++中,类型的概念是基础,对于枚举也是如此,所以会进行类型检查。当创建一个命名的枚举时,就像使用类一样有效地创建了一个新类型,在单元翻译期,枚举名成为保留字。

A: 此外,C++对枚举的类型检查比在C中更为严格,如果有一个color枚举类型的实例a,在C中,可以写成a++,但在C++中不能这样写,这是因为枚举的自增运算执行两种类型转换,首先枚举的值隐式地从color强制转换为int,然后递增该值,再把int强制转换回color类型。在C++中这是不允许的,因为color是一个独特的类型,并不等价于int,这一点是有意义的,因为我们怎么能知道颜色表中blue的增量值会是什么?如果想对color进行增量运算,则它应该是一个类而不是一个enum,成为一个类会更加安全。

A: C++中任何时候写代码对enum类型进行隐式转换,编译器都会标记这是一个危险活动。

Q: 用union节省内存?

A: 有时一个程序会使用同一个变量处理不同的数据类型。对于这种情况,有两种选择,可以创建struct,其中包含需要存储的所有可能的不同类型,或者可以使用union(联合)。

A: union把所有的数据放进一个单独的空间内,它计算出放在union中的最大项所必需的空间数,并生成union的大小,使用union可以节省内存。

A: 每当在union中放置一个值,这个值总是放在union开始的同一个地方,但是只使用必需的空间,因此我们创建的是一个能容纳任何一个union的“超变量”,所有union变量的地址都是一样,而在类或struct中地址是不同的。

A: 示例:union.cpp,运行结果link,试着去掉不同的元素,看看对union的大小有什么影响,注意在union中声明数据类型的多个实例是没有意义的。

A: 编译器根据所选择的联合的成员执行适当的赋值,一旦进行赋值,编译器并不关心用联合做什么。在上面的示例中,可以对x赋一个浮点值:

x.f = 2.222;

然后把它作为一个int输出

cout << x.i;    

结果是无用的信息。

Q: 数组(一)?

A: 数组是一种复合类型,因为它允许在一个单一的标识符下把变量结合在一起,一个接着一个。

A: 如:int a[10];就为10个int变量创建了一个接一个的存储空间,但是每一个变量并没有单独的标识符。相反它们都集结在名字a下。

A: 要访问一个数组元素,可以使用定义数组时所使用的方括号语法:

a[5] = 47;

访问数组很快,但是如果下标超出数组的界限,这就不安全了,这可能会访问到别的变量;另一个缺陷是必须在编译期定义数组的大小。

A: 如果想在运行期改变大小,则不能使用上面的语法,C有一种动态创建数组的方式,但是这会造成严重的混乱。

A: 示例:structarray.cpp,运行结果link,注意,struct中的标识符i与for循环中的i无关。

Q: 数组(二)?

A: 为了知道数组中的相邻元素的距离,可以在上面的示例上,打印出每个元素地址:

A: 示例:structarray.cpp,运行结果link

A: 当运行程序时,会看到每一个元素和前一个元素都是相距一个ThreeDpoint结构体大小的距离,也就是说,它们是一个接一个存放的。

Q: 数组(三)?

A: 数组的标识符不像一般变量的标识符。数组标识符不是左值,不能给它赋值,它只是一个进入方括号语法的手段,当给出数组名而没有方括号时,得到的就是数组的起始地址。

A: 示例:ArrayIdentifier.cpp,运行结果link,会看到这两个地址是一样的。

Q: 数组(四)?

A: 因此可以把数组标识符看作是数组起始处的只读指针,尽管不能改变数组标识符的指向,但是可以另创建指针,使它在数组中移动。事实上,方括号语法和指针一样工作:

示例:PointersAndBrackets.cpp,运行结果link

Q: 数组(五)?

A: 给一个函数传递数组时,命名数组以产生它的起始地址的事实相当重要。

A: 如果声明一个数组为函数参数,实际上真正声明的是一个指针。

A: 示例:ArrayArgs.cpp,运行结果link

A: 尽管func1()和func2()以不同的方式声明它们的参数,但是在函数内部的用法是一样的,这个例子暴露出了一些别的问题:数组不可以按值传递,也就是说,不会自动得到传递给函数的数组的本地拷贝,因此修改数组时,一直是在修改外部对象。

A: 我们会注意到,print()对数组参数使用方括号语法,尽管把数组作为参数传递时,指针语法和方括号语法一样,但是方括号语法使得读者更清楚它的意思是把这个参数看作是一个数组。

A: 值得注意,仅仅传递数组的地址还不能提供足够的信息,必须知道在函数中数组有多大,这样就不会超出数组的边界。

Q: 数组(六)?

A: 指针数组,arrays of pointers,命令行参数,C/C++的函数main的参数表,形式如下:

int main(int argc, char* argv[]) 
{  // ...

A: 第一个参数的值是第二个参数的数组元素个数。命令行中的每一个用空格分隔的字符串被转换为单独的数组参数。

A: 示例:CommandLineArgs.cpp,运行结果link

A: argv[0]是程序本身的路径和名字,它允许程序发现自己的信息。

A: 还有另一种声明argv的方式:

int main(int argc, char** argv) 
{  //...

两种形式是等价的。

Q: 探究浮点格式?

A: TODO...

Q: 指针的算术用法(一)?

A: 如果用指针所做的工作只是看作是数组的别名,那么指向数组的指针可能不太令人感兴趣。但是,指针比这个更灵活,因为可以修改它们指向任何别的地方,但是记住,不能修改数组标识符来指向别的地方。

A: 指针算术(pointer arithmetic)指的是对指针的某些算术运算符的应用。例如,指针常用的运算符是++,即给指针加1,它的实际意义是改变指针,移向下一个值。

A: 示例:PointerIncrement.cpp,运行结果link

A: 这就是指针算术的技巧:编译器计算出指针改变的正确值,使它指向数组中的下一个元素。

Q: 指针的算术用法(二)?

A: 甚至可以在struct数组中使用指针。

A: 示例:PointerIncrement2.cpp,运行结果link

A: 编译器对struct、class、union指针也能正确地工作。

Q: 指针的算术用法(三)?

A: 指针的算术运算有:++、--、+、-。

A: 但是,后面两个运算符的使用有限制:不能把两个指针相加;如果指针相减,其结果是两个指针之前相隔的元素个数;不过一个指针可以加上或减去一个整数。

A: 示例:PointerArithmetic.cpp,运行结果link

A: 预处理宏里面的一个#的作用:就是获得任何一个表达式都会把它转化为一个字符串。

A: 在各种情况下,指针算术根据所指的元素大小调整指针,使其指向正确的地方。

调试技巧

可能会在没有调试器,例如一个嵌入式系统的环境下进行开发,在这种情况下,就要用创造性的方法去发现和显示关于程序执行情况的信息。

Q: 调试标记?

A: 如果在程序中加入调试代码,可能引起不便,一开始得到了太多信息,这使得很难把故障孤立出来。当认为已经找到了故障时,我们开始删掉调试代码,却有可能发现在需要这些代码,我们可以用两种标记解决这类问题:预处理器调试标记和运行期调试标记。

Q: 预处理器调试标记?

A: 通过使用预处理器#define定义一个或更多的调试标记,在头文件中更适合,可以测试一个使用#ifdef语句和包含条件调试代码的标记。当认为调试完成了,只需要使用#undef标记,代码就自动消失,这会减少可执行文件的大小和运行时间。

A: 最好在开始建立工程前决定调试标记的名字,这样名字一致。为了区分预处理标记和变量,预处理标记一般用大写字母书写。一个常用的标记名是DEBUG,但是小心,不能使用NDEBUG,它是C中的保留字。

A: 代码的形式如下:

// Probably in a header file
#define DEBUG
// ...
#ifdef DEBUG
/* debugging code here */
#endif // DEBUG

A: 大多数C/C++的程序实现还允许在编译器命令行中使用#define和#undef标记,所以可以用一个单独的命令重新编译代码并插入调试信息,最好使用makefile。

###Q: 运行期调试标记?
A: 在某些情况下,在程序执行期间打开和关闭调试标记会更加方便,特别是使用命令行在启动程序时设置它们。只是为了插入调试代码来重新编译个大程序是很乏味的。

A: 示例:DynamicDebugFlags.cpp

A: 为了自动打开和关闭调试代码,可以建立一个全局的bool标记,注意使用小写字母书写变量,用来提醒读者它不是一个预处理器标记。

###Q: 把变量和表达式转换成字符串?
A: 写调试代码的时候,编写由包含变量名和后跟变量的字符串组成的打印表达式时很乏味的,幸运的是,标准C提供了‘#’,在一个预处理器宏中的参数前使用一个#,预处理器会把这个参数转换成一个字符串。把这一点与没有插入标点符号的若干个字符串结合而连接成一个单独的字符串,能够生成一个十分方便的宏用于调试期间打印这个变量的值。

A: 示例:StringizingExpressions.cpp,运行结果link,示例中使用一个宏创建了一种速记方式打印出字符串化的表达式,然后计算表达式并打印出结果。

A: 当不想调试时,也可以插入一个#ifdef使得定义的P(A)不起作用。

Q: C语言的assert()宏?

A: 在标准头文件中,会发现assert()是一个方便的调试宏。当使用assert()时,给它一个参数,即一个表达为真的表达式。预处理器产生测试该断言的代码。如果断言不为真,则发出一个错误信息告诉断言是什么以及它失败之后程序会终止。

A: 示例:assert.cpp,运行结果link

A: 这个宏来源于标准C,所以在头文件assert.h中也可以使用。

当完成调试后,通过在程序的#include <cassert>之前插入语句行:

#define NODEBUG

或者在编译器命令行中定义ndebug,可以消除宏产生的代码。在<cassert>中使用的ndebug是一个标记,用来改变宏产生代码的方式。

函数地址

Q: 什么是函数指针?

一旦函数被编译并载入计算机中执行,它就会占用一块内存。
这段存储空间的首地址称为这个函数的地址,而且函数名表示的就是这个地址。
既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

Q: 如何定义函数指针?

A: 要定义一个指针指向一个无参无返回值的函数,可以写成:

void (*funcPtr)();

A: 当看到像这样的一个复杂定义时,最好的处理方法是从中间开始和向外扩展。

A: “从中间开始”的意思是从变量名开始,这里指funcPtr。

A: “向外扩展”的意思是先注意右边最近的项,然后注意左边,然后再右边,再左边。大多数声明都是以右-左-右动作的方式工作的。

A: 现在我们回过头来看,从中间开始(funPtr是一个...),向右边走(没有东西,被右括号拦住了),向左边走并发现一个*...指针指向一个...),向右边走并发现一个空参数列表(...没有参数的函数...),向左边走并发现一个void(funcPtr是一个指针,它指向一个不带参数并返回void的函数,funcPtr的类型为 void(*)())。

A: 所以函数指针的定义方式为:

函数返回值类型 (* 指针变量名) (函数参数列表);

A: 有人可能感到奇怪为什么*funcPtr需要括号,如果不使用括号,编译器会看到:

void *funcPtr();

这是在声明一个返回类型为void*的函数而不是定义一个变量。

A: 在了解和定义应该是什么时候,可以想象编译器要经历同样的过程,所以要遇到这些括号,使得编译器会返回左边并发现*,而不是一直向右发现一个空参数表。这就是指针函数和函数指针的区别。

Q: 复杂的函数声明和定义?

A: 示例:ComplicatedDefinitions.cpp

A: 使用先右后左的原则去推断。

A: 我们可能很少甚至是从未使用过如此复杂的声明和定义。但如果通过练习能把它搞清楚的话,就不会被在现实生活中可能遇到的稍微复杂的情况所困惑。

Q: 使用函数指针?

A: 示例:PointerToFunction.cpp,运行结果link

Q: 指向函数的指针数组?

A: Arrays of pointers to functions

A: 为了选择一个函数,只需要使用数组的下标,然后间接引用这个指针,这种方式支持表格式驱动码(table-driven code)的概念。

A: 可以根据状态变量或者状态变量的组合值去选择被执行函数,而不用条件语句或case语句,这种设计方式对于经常要从表中添加或删除函数(或者想动态创建或改变表)十分有用。

A: 示例:FunctionTable.cpp,运行结果link,示例中使用预处理宏创建了一些哑函数(dummy function),然后使用自动聚合(automatic aggregate)初始化功能创建指向这些函数的指针数组。正如看到的那样,可以很容易从表中添加或删除函数而只需修改少量的代码。

A: 当希望创建一些解释器或表处理程序时,可以借鉴这种技术。

make:管理分段编译

Q: 仅仅修改某个文件却要重新编译所有文件?

A: 当使用分段编译(separate compilcation,即把代码拆分为许多翻译单元)时,需要某种方法自动编译每个文件并且告诉链接器把所有分散的代码段,连同适当的库和启动代码,构造成一个可执行的文件。

A: 许多编译器允许使用一个简单的命令行语句完成,例如,对于GNU C++编译器,可能会用到:

g++ a.cpp b.cpp c.cpp d.cpp

使用这种方法的问题是编译器事先要编译每个文件而不管文件是否需要重建,在具有许多文件的工程中,如果仅仅修改了一个文件,就可能不得不重新编译所有的文件。

Q: make工具?

A: 解决上面的问题的方法是用一个称为make的工具。该程序是在UNIX上开发的。

A: make工具按照一个名为Makefile的文件中的指令去管理一个工程中的所有单个文件。

A: 当编辑了工程中的某些文件并使用了make时,make程序会按照makefile中的说明去比较源代码文件与相应目标文件的日期,如果源代码文件的日期比它的目标文件的日期新,make就会调用编译器对源代码进行编译。make仅仅编译已经改变了的源代码,以及其他受修改文件影响的源代码文件。

A: 使用make程序,每次修改程序时,不必重新编译工程中的所有文件,也不必核对所有生成的东西。

A: Makefile文件包含了组合工程的所有命令,学会使用make命令会节省大量的时间,也会减少挫折。

Q: make的行为?

A: 当输入make时,make程序在当前目录中寻找名为Makefile的文件,这个文件列出了源代码文件间的依赖关系。

A: make程序观察文件的日期,如果一个依赖文件的日期比它所依赖的文件旧,make程序执行依赖关系之后列出的规则。
Alt text

A: 作为一个简单示例,一个名为hello的程序的Makefile文件可能包含:

# A comment
hello.exe: hello.cpp
    mycompiler hello.cpp

这就是说hello.exe目标文件依赖于hello.cpp。当hello.cpp比hello.exe文件日期新时,make执行“规则”mycompiler hello.cpp。

A: 可能会有多重依赖和多重规则。

A: 许多make程序要求所有规则以tab键开头,而不是4个空格。

A: 规则不仅局限于调用编译器,在make中还可以调用想要调用的任何程序。

Q: Makefile中使用变量?

A: 例如:

CPP = mycompiler
hello.exe: hello.cpp
    $(CPP) hello.cpp

变量设置的格式是varName = content,以后要引用这个变量,只需要用和圆括号即可,(varName)。

对于上面的变量名CPP,如果想改变不同的编译器,只需把变量CPP赋值不同的编译器即可,如:

CPP = g++

Q: 后缀规则?

A: 因为make的设计注重于节约时间,所以只要依赖于文件名字的后缀,它就有一种简化操作的方式,这些简化被称为后缀规则(suffix rules)。

A: 一条后缀规则是教make怎么样从一种类型文件(如.cpp)转化为另一类型(.obj)的方法。一旦有了make从一种文件转化为另一种文件的规则,其他要做的只是告诉make哪些文件依赖于其他文件。

A: 后缀规则告诉make可以根据文件的扩展名去考虑怎样构建程序而不需用明显规则去构建一切。在这种情况下它指出:“调用下面的命令从扩展名为cpp的文件去构造扩展名为exe的文件”,例如:

CPP = mycompiler
.SUFFIXES: .exe .cpp
.cpp.exe:
    ${CPP} $<

A: .SUFFIXES指令告诉make必须注意后面的扩展名,因为它们对于这个特定的Makefile有特殊的意义。

A: 看到后缀规则.cpp .exe,说明“这里是怎么把任何扩展名为cpp文件转化为一个扩展名为exe的文件”。( 当cpp文件比exe文件新的时候。)

A: $<是make内置的特殊变量,该变量只能用于后缀规则,意思是“无论怎样都要出发的规则”,有时称为依赖。在本例中表示“需要被编译的cpp文件”。

A: 一旦建立了后缀规则,就能简单地说明,例如make Control.exe,后缀规则会展开,即使在整个Makefile文件中未提及“Control”。

Q: 默认目标?

A: 在$(CPP) $<之后,make在文件中查找第一个“目标”,并构建它,除非指定了不同的目标文件,因此对于Makefile文件:

CPP = mycompiler
.SUFFIXES: .exe .cpp
.cpp.exe:
    ${CPP} $<
target1.exe:
target2.exe:

A: 如果简单地输入make,那么就生成target1.exe,因为它是make遇到的第一个目标。为了生成target2.exe我们不得不显示说明make target2.exe,这样做就比较冗长,因此通常会创建一个依赖于所有其余目标文件的默认“哑元”目标。例如:

CPP = mycompiler
.SUFFIXES: .exe .cpp
.cpp.exe:
    ${CPP} $<
all: target1.exe target2.exe

A: 在这里,all并不存在,没有名为all的文件,因此每次键入make,它会把all作为第一个目标,这是默认的目标,然后发现all不存在,所以它检查所有的依赖关系。

A: 因此它查看target1.exe并使用后缀规则判断:1) target1.exe文件是否存在 2) target1.cpp文件是否比target1.exe文件新。如果1)和2)都成立,就使用后缀规则。然后在默认的目标列表上查找下一个目标文件。因此通过建立一个默认的目标文件列表,只需要简单输入make就能够生成在工程中的所有可执行文件。

A: 按习惯通常将第一个目标,也就是默认目标习惯命名为all,但可以随便起名。

A: 此外,可以定义其他的非默认目标文件列表用于其他目的,例如,当键入make debug时会重新构建所有带有调试信息的文件。

Q: Makefile简单示例?

A:

CPP = g++
OFLAG = -o
.SUFFIXES : .o .cpp .c
.cpp.o :
  $(CPP) $(CPPFLAGS) -c $<
.c.o :
  $(CPP) $(CPPFLAGS) -c $<

all: \
  Return \
  Declare \
  Ifthen \
  Guess \
  Guess2
# Rest of the files for this chapter not shown

Return: Return.o 
  $(CPP) $(OFLAG)Return Return.o 

Declare: Declare.o 
  $(CPP) $(OFLAG)Declare Declare.o 

Ifthen: Ifthen.o 
  $(CPP) $(OFLAG)Ifthen Ifthen.o 

Guess: Guess.o 
  $(CPP) $(OFLAG)Guess Guess.o 

Guess2: Guess2.o 
  $(CPP) $(OFLAG)Guess2 Guess2.o 

Return.o: Return.cpp 
Declare.o: Declare.cpp 
Ifthen.o: Ifthen.cpp 
Guess.o: Guess.cpp 
Guess2.o: Guess2.cpp

A: CPP变量被设置为编译器的名字,为了使用不同的编译器,可以编辑Makefile文件,或者在命令行上修改该变量的值,例如:

make CPP=g++

A: 可以看出本例有两条后缀规则,一条用于cpp文件,另一条用于.c文件。默认的目标是all,对于目标的所有的行用反斜线符号表示继续,直到Guess2,它是目标列表中的最后一行,因此不需要反斜线符。

A: 后缀规则管理从cpp文件创建目标文件(以.o作为扩展名),但是通常对创建可执行文件需要有显示说明的规则,因为一个可执行文件通常是通过链接许多不同的目标文件而产生的,而make程序不知道哪些是目标文件。

A: 同样,在某些情况下,如Linux/Unix下,对于可执行文件并无标准扩展名,这种情况下,后缀规则将不能工作,所以我们发现创建最终的执行文件都显示说明了规则。

A: 更多可以参考Oram和Talbott所著的《Managing Projects with Make》(O'Reilly, 1993)

posted @ 2019-08-19 06:42  fireway  阅读(715)  评论(0编辑  收藏  举报