《Thinking in C++, 2nd Edition》笔记-第三章(The C in C++)

1 函数参数

在早期的C语言中,你可以用随意的用任何数量和类型的参数来调用函数,编译器不会不允许。

标准的C&C++使用了一个叫函数原型的特性。在声明和定义函数时,必须指定函数的参数类型和返回值类型,在调用时如果参数或返回值不对,编译器会提示错误。

以下两种函数声明都是允许的:

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

C语言在函数定义时,参数的名字是必须的,因为在函数里需要用来这些参数。但在C++中,定义函数时,参数是可以不命名的,在函数中也无法使用它,因为它是用来在参数列表中保留位置的。当然,也可以在函数中对命名的参数不作使用,但是这样编译器会出现一个“参数会被使用”的警告。

fun()   在C中,表示的是“未决定数量的参数(这时会关闭类型检查)”;而在C++中,这表示的是没有参数。而fun(void)在C或C++中都表示的没有参数。

在不确定参数类型或数量时还有一种参数列表选择:可变参数列表variable argument list)。用...来表示。这个用来很复杂。在确定参数列表但不想进行参数检查时可以使用它,因此你应该限制它在C中的应用,避免在C++中使用。


2 函数返回值

C++函数必须指定返回值类型,而C语言你可以让它默认为int型。


3 使用C函数库

有些方便使用的函数可能不是标准的C库的一部分,如果不考虑平台移植的话,这没有影响。否则的话,应该严格使用标准库函数,或者将那些代码隔离在某个地方,以便在移植时修改方便。在C++中,通常把它封装成类。


4 控制流程

C++使用了C中的所有流程控制语句,包括ifelse, while, do-while, for,switch, goto。


true和false

C++中一个表达式会产生一个布尔型变量,true或flase, 而C中一个表达式是非零值即表示"true"。


if-else

if(expression)
   statement


if(expression)
    statement
else
    statement


while

while(expression)
    statement


do-while

do
    statement
while(expression);


for

for(initialization; conditional; step)
    statement


break和continue关键字

break:不执行之后的代码直接跳出循环。

continue:不执行之后的代码进入下一次循环。


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;
}

Selector是一个产生整型值的表达式,switch用来和selector比较每一个整型值,如果没有匹配的,进入default语句执行。

break通常是必要的,如果某个分支没有break,程序会顺序执行(下一个分支)直到遇到break。


5 操作符

操作符可以当作一种特殊的函数。+,-,*,/, = 通常在任何编程语言里都表示相同的意思。


优先级(Precedence)

操作符的优先级定义了几个不同的操作符同时出现时,求值的顺序。比如乘除优先于加减。


自增和自减

“--”表示减小一个单元,“++”表示增加一个单元。如果A是整型,那么++A先执行A=A+1,然后将A的值当作一个结果值;如果A++,那么先将A作为一个结果值,然后再执行A=A+1。


6 数据类型

数据类型定义了使用内存的方式。可以是内置的或抽象的。内置类型是编译器固有能解析的。相反的,一个用户通常用类来定义的数据类型,通常被称作抽象数据类型(ADT)。

标准C对内置数据类型并没有规定该类型占据几个数据位,而是规定了最大最小值。在二进制基础的机器上,这也可以转化为数据位数。


布尔值,true, false

引入布尔值,以下语言元素也随之与bool相匹配:

&&, ||, !    对布尔值操作,并且产生布尔值

<, >, >=, <=, == , !=         产生布尔值

if, for, while, do                 将表达式转化为布尔值

?:                                       将第一个操作数转化为布尔值

由于很多已经存在的代码中将int作为标志,所以编译器会隐式的将int 转为 bool。


指针

程序运行之后程序里的所有元素都会被加载到内存中的某个位置。每块空间都可以通过地址来标识(区别于其它空间)。“&”操作符用来告诉你一个元素的地址。如何使用这个地址?最重要的就是把它存在另一个变量里以供将来使用,这个特殊的变量类型就是指针。

int* ip; // ip points to an int variable

一般的C++编程指南会要求定义指针的时候将它初始化。


引用

通常在往函数里传参数时,参数的一个拷贝在函数中被创建。这就是传值(pass-by-value)。传入的这个拷贝是个局部变量,在函数执行完成后会自动销毁。每次函数被调用时,一份临时空间被创建用来存放这个局部变量,并且被初始化为传入的实际值。在函数中改变这个值是无法改变外部的原始值的。如果想改变外部的值的话,可以传入指针。

C和C++中都可以传指针,但是C++有另外一种传地址的方式:传引用(pass-by-reference)。它的效果其实是取地址,然后将地址传入到函数中,因此也可以改变外部的值。

还有一种可以和指针一起使用的类型:void。如果一个指针是void*,那表示任何类型的地址都可以赋给它。一旦赋给void*,就将丢失类型信息,因此在下次使用时,需要进行正确的类型转换。


7 作用域(scoping)

作用域规则确定了一个变量变量何时有效,何时创建,何时销毁。一个变量的作用域从距离定义它最近的开括号到与之匹配的闭括号,也就是最近的一组大括号。

// How variables are scoped
int main() {
    int scp1;
    // scp1 visible here
    {
        // scp1 still visible here
        //.....
        int scp2;
        // scp2 visible here
        //.....
        {
	    // scp1 & scp2 still visible here
	    //..
	    int scp3;
	    // scp1, scp2 & scp3 visible here
	    // ...
	} // <-- scp3 destroyed here
	// scp3 not available here
	// scp1 & scp2 still visible here
	// ...
    } // <-- scp2 destroyed here
    // scp3 & scp2 not available here
    // scp1 still visible here
    //..
} // <-- scp1 destroyed here
///:~

对于for(int i = 0; i < 100; i++) i的作用域就是这个循环控制结构的作用域。


8 指定存储分配

全局变量

全局变量在所有函数体之外定义,并且能在程序任何地方使用(包括在其它文件的代码),它没有作用域影响并且总是可用。

如果在一个文件中使用另一个文件中定义的全局变量,需要使用extern来声明这个变量,用来向编译器表明这个变量在别的地方存在。


局部变量

局部变量在一个作用域内存在,对函数来说是局部的。


寄存器变量(register variables)

register关键字告诉编译器尽可能快的读取这个变量,它通常放在寄存器中。使用有很多限制,不能取或计算寄存器变量的地址;只有在程序块中声明(不能用static),但是你可以把它用作函数的参数。不要试图优化它,因为编译器的优化会做得很好。因此,register关键字最好避免使用。


static

通常一个函数里定义的局部变量在出了函数的作用域后会消失,当再次访问函数时,变量会再次被创建并且值被重新初始化。如果想让这个变量在程序执行期间始终存在,可以将它声明为static,并给它一个初值。初始化只会在第一次调用时执行,之后它的值会在函数调用时保持。它在函数之外不可用,这是之所以不使用全局变量的原因。

用static声明一个函数或所有函数之外的变量,表明这个函数或变量只有在当前文件中使用,称作file scope。


extern

extern告诉编译器一个函数或变量已经存在,即使在编译这个文件时还没有看到它,它可能在别的文件中或该文件后面进行了定义。


Linkage

在执行的程序中,有两种连接方式:内部连接和外部连接。

内连接:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量.C/C++中内连接使用static关键字指定.
外连接:所有被编译过的文件创建一片单独存储空间.一旦空间被创建,连接器必须解决对这片存储空间的引用.全局变量和函数使用外部连接.通过extern关键字声明,可以从其他文件访问相应的变量和函数.
全局变量是外部连接,所有函数体外定义的变量(除了C++中的const)和函数都是默认为外部连接的。可以用static特别指定为内部连接。也可以通过extern显示的表明一个名称是外部连接。在C中定义不需要用extern,但是在C++中有时需要(const)。


Constants

在非标准的C中,定义常量只能用宏定义。C++引入了命名常量,比如const int i = 10;这意味着i将无法被修改。C和C++中的const预期用途是不同的。

在C中,const是一个不能被改变的普通变量,既然是变量,就要占用存储空间,所以编译器不知道编译时的值。

C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中。

const int n = 1;
int array[n] = { 0 };


这段代码在C++中没问题,在C中编译不通过。

在C语言中:
const int size;
这个语句是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间.但在C++中这样写是不正确的.C++中const默认是内部连接,如果想在C++中达到以上的效果,必须要用extern关键字.
C++中,const默认使用内部连接.而C中使用外部连接.


volatile

volatile告诉编译器“你永远不知道它何时被修改”,这样阻止编译器把它当作确定变量的优化。一个volatile变量在需要时随时读取,即使你刚刚在前一行读了它。

volatile 影响编译器编译的结果,指出,volatile 变量是随时可能发生变化的,与volatile变量有关的运算,不要进行编译优化,以免出错,(VC++ 在产生release版可执行码时会进行编译优化,加volatile关键字的变量有关的运算,将不进行编译优化。)。
例如:

volatile int i=10; 
int j = i; 
... 
int k = i; 

 

volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的可执行码会重新从i的地址读取数据放在k中。
而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在k中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问,不会出错。


9 操作符的使用

赋值

赋值操作符的右值可以是任何常数,变量或产生值的表达式,但左值必须是一个变量(意味着必须有物理空间来存储数据)。


数学操作符

+, -, *, / , %, +=, -=, *=, /=


关系算子

<, >, >=, <=, ==, !=


逻辑运算符

&&, ||


位运算符

与&, 或 |, 异或^,非~


移位操作符

左移<<,右移>>,位操作符通常是很高效的,因为通常直接转化为汇编代码,一个C或C++语句有时只会产生一行汇编代码。


一元运算符

+,-,&(取地址), 解引用(*, ->)


三元运算符

?:它其实就是一个if-else。


逗号运算符

多个表达式可以用逗号分开,其中用逗号分开的表达式的值分别结算,但整个表达式的值是最后一个表达式的值。


转型操作符

int b = 200;
unsigned long a = (unsigned long int)b;


C++显式转型

static_cast <new_type> (expression)
const_cast <new_type> (expression)
dynamic_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)

static_cast

static_cast 很像 C 语言中的旧式类型转换。它能进行基础类型之间的转换,也能将带有可被单参调用的构造函数或用户自定义类型转换操作符的类型转换,还能在存有继承关系的类之间进行转换(即可将基类转换为子类,也可将子类转换为基类),还能将 non-const对象转换为 const对象(注意:反之则不行,那是const_cast的职责。)。

注意:static_cast 转换时并不进行运行时安全检查,所以是非安全的,很容易出问题。因此 C++ 引入 dynamic_cast 来处理安全转型。


dynamic_cast

dynamic_cast 主要用来在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。如果转型失败会返回null(转型对象为指针时)或抛出异常(转型对象为引用时)。dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此 dynamic_cast 存在一定的效率损失。dynamic_cast 只有在基类带有虚函数的情况下才允许将基类转换为子类

dynamic_cast 也可在 null 指针和指向其他类型的指针之间进行转换,也可以将指向类型的指针转换为 void 指针(基于此,我们可以获取一个对象的内存起始地址const void * rawAddress = dynamic_cast<const void *> (this);)。


const_cast

前面提到 const_cast 可去除对象的常量性(const),它还可以去除对象的易变性(volatile)。const_cast 的唯一职责就在于此,若将 const_cast 用于其他转型将会报错。


reinterpret_cast

reinterpret_cast 用来执行低级转型,如将执行一个 int 的指针强转为 int。其转换结果与编译平台息息相关,不具有可移植性,因此在一般的代码中不常见到它。reinterpret_cast 常用的一个用途是转换函数指针类型,即可以将一种类型的函数指针转换为另一种类型的函数指针,但这种转换可能会导致不正确的结果。总之,reinterpret_cast 只用于底层代码,一般我们都用不到它,如果你的代码中使用到这种转型,务必明白自己在干什么。


sizeof

sizeof用来获得数据占用的内存大小。


asm关键字

允许你在C++程序中写汇编代码,比如为了提高效率或特殊的处理器代码。


10 复合类型创建

typedef来使用别名

typedef existing-type-description alias-name


用结构体来组合变量

struct用来将一组变量收集到一个结构体中,然后你可以用这个你创建出来的新类型的实例。

struct Structure1 {
char c;
};
int main() {
struct Structure1 s1, s2;
s1.c = 'a'; // Select an element
}

注意,在C中,不能直接用Structure1 s1直接定义s1, 必须前面加上struct。为此,C使用typedef,如下所示。

typedef struct {
char c;
} Structure2;
int main() {
Structure2 s1, s2;
s1.c = 'a';
}
typedef struct SelfReferential {
int i;
SelfReferential* sr; // Head spinning yet?
} SelfReferential;


枚举

枚举是一种将名字和数量相关联的方法,因此人们在读代码时可以知道更多的意思。

enum ShapeType {
circle,
square,
rectangle
}; // Must end with a semicolon like a struct
enum ShapeType {
circle = 10, square = 20, rectangle = 50
};

C++中的枚举有更强的类型检查,比如枚举color的实例a,在C中可以a++,但在C++中不行。


用union来节省内存

有时需要用一个变量的不同种类型来处理问题,这时可以用结构体或枚举。结构体中包含所有的可能的类型,创建出所有内存;而联合把所有数据放在一个地方,它计算出可能使用的类型中的最大内存放在联合中,并把它作为联合的内存。


数组

int a[10];

访问一个元素可以用a[5]。它的访问速度很快,但是你的索引可能超过数组末尾,它会指向别的变量。另外,数组的大小必须在编译时就定义,在运行期你无法改变数组大小。


11 函数地址

void (*funcPtr)();

看到这种定义时,最好的理解方法是:从中间开始,向两边开始读。也就是从funcPtr开始,往右(什么也没有,被)阻断),往左(一个*,表示一个指针),往右(一个空参数,表示不带参数的指针),往左(void,表示没有返回值)。因此连起来就是,funPtr是一个指针,指向一个函数,不带参数,不带返回值。即:一个不带参数和返回值的函数指针。

void * (*(*fp1)(int))[10];
fp1是一个函数指针,它指向参数为int,返回值为指向10维void指针数组的指针。

float (*(*fp2)(int,int,float))(int);

fp2是一个函数指针,它指向一个参数为(int,int,float),返回值为一个函数指针(参数为int,返回值为float)。

 

函数指针使用

void func() {
cout << "func() called..." << endl;
}
int main() {
void (*fp)(); // Define a function pointer
fp = func; // Initialize it
(*fp)(); // Dereferencing calls the function
void (*fp2)() = func; // Define and initialize
(*fp2)();
}


函数指针数组使用

// A macro to define dummy functions:
#define DF(N) void N() { \
cout << "function " #N " called..." << endl; }
DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g);
void (*func_table[])() = { a, b, c, d, e, f, g };
int main() {
while(1) {
    cout << "press a key from 'a' to 'g' "
        "or q to quit" << endl;
    char c, cr;
    cin.get(c); cin.get(cr); // second one for CR
    if ( c == 'q' )
        break; // ... out of while(1)
    if ( c < 'a' || c > 'g' )
        continue;
    (*func_table[c - 'a'])();
}
}


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值