-------------------------------------------------基本原理----------------------------------------------------
C++程序设计三大难点:预处理、counst与sizeof
1. 预处理
C++从C语言那里把C语言预处理器继承过来(C语言预处理器,被Bjarne博士简称为Cpp)。
主要作用就是: 把通过预处理的内建功能对一个资源进行等价替换.
最常见的预处理有: 文件包含,条件编译、布局控制和宏替换4种。
文件包含: #include 是一种最为常见的预处理,主要是做为文件的引用组合源程序正文。
条件编译: #if,#ifndef,#ifdef,#endif,#undef等也是比较常见的预处理,主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。
布局控制: #progma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。
宏替换: #define,这是最常见的用法,它可以定义符号常量、函数功能、重新命名、字符串的拼接等各种功能。
(1)简单的宏定义
#define <宏名> <字符串>
例: #define PI 3.1415926
(2) 带参数的宏定义
#define <宏名> (<参数表>) <宏体>
例: #define A(x) x
常见的预处理指令:
#define 宏定义
#undef 取消宏
#include 文本包含
#ifdef 如果宏被定义就进行编译
#ifndef 如果宏未被定义就进行编译
#endif 结束编译块的控制
#if 表达式非零就对代码进行编译
#else 作为其他预处理的剩余选项进行编译
#elif 这是一种#else和#if的组合选项
#line 改变当前的行数和文件名称
#error 输出一个错误信息
#pragma 为编译程序提供非常规的控制流信息
2. const
Const 是C++中常用的类型修饰符,常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。
关于C++ const 的全面总结
Const主要有几点优势:
保护功能:便于进行类型检查;可以保护被修饰的东西;为函数重载提供了一个参考
时空效率:相比宏常量的反复分配内存空间,const常量只分配一次内存空间;编译器将它们保存在符号表中,成为编译常量,没有了存储与读取的操作。
指针使用CONST
(1)指针本身是常量不可变
(char*) const pContent;
const (char*) pContent;
(2)指针所指向的内容是常量不可变
const (char) *pContent;
(char) const *pContent;
(3)两者都不可变
const char* const pContent;
(4)还有其中区别方法,沿着*号划一条线:
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;(const在左—>内容常量)
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。(const在右—>指针常量)
函数中使用CONST
任何不修改数据成员的函数都应该被定义为const成员函数。
常量函数结尾使用const,表明该函数不会修改调用的实参。
像一般的输出函数display(),print(),show()等都可定义为常量函数。
如:int size() const;
如果const放在函数开头,一般用来表示返回一个常量。
a.传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)
void function(const int Var);
b.参数指针所指内容为常量不可变
void function(const char* Var);
c.参数指针本身为常量不可变(也无意义,因为char* Var也是形参)
void function(char* const Var);
d.参数为引用,为了增加效率同时防止修改。修饰引用参数时:
void function(const Class& Var); //引用参数在函数内不可以改变
将Const类型转化为非Const类型的方法
采用const_cast 进行转换。
用法:const_cast <type_id> (expression)
类里面的成员变量加mutable修饰const成员函数,就可以修改它了。
对比学习内容:volatile
与const一样,volatile是一个类型修饰符。如果不加入volatile:要么无法编写多线程程序,要么编译器失去大量优化的机会。
volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
使用该关键字的例子如下:
volatile int vint;
当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。
而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。
2). 一个指针可以是volatile 吗?解释为什么。
3). 下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题:
int square(volatile int *ptr)
{
return ((*ptr) * (*ptr));
}
下面是答案:
1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2). 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。
3). 这段代码是个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int* &ptr)//这里参数应该申明为引用,不然函数体里只会使用副本,外部没法更改
{
int a,b;
a = *ptr;
b = *ptr;
return a*b;
}
由于*ptr的值可能在两次取值语句之间发生改变,因此a和b可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:
long square(volatile int*ptr)
{
int a;
a = *ptr;
return a*a;
}
3. sizeof
C++ sizeof用法 .
sizeof不是函数,更像一个特殊的宏,它是在编译阶段求值的。
cout<<sizeof(int)<<endl; // 32位机上int长度为4 cout<<sizeof(1==2)<<endl; // == 操作符返回bool类型,相当于 cout<<sizeof(bool)<<endl;在编译阶段已经被翻译为:
cout<<4<<endl; cout<<1<<endl;
这里有个陷阱,看下面的程序:
int a = 0; cout<<sizeof(a=3)<<endl; cout<<a<<endl;输出为什么是4,0而不是期望中的4,3???就在于sizeof在编译阶段处理的特性。由于sizeof不能被编译成机器码,所以sizeof作用范围内,也就是()里面的内容也不能被编译,而是被替换成类型。=操作符返回左操作数的类型,所以a=3相当于int,而代码也被替换为:
int a = 0; cout<<4<<endl; cout<<a<<endl;
sizeof有两种用法:
(1)sizeof(object)
也就是对对象使用sizeof,也可以写成sizeof object 的形式。
(2)sizeof(typename)
也就是对类型使用sizeof,注意这种情况下写成sizeof typename是非法的。
任何时候,加括号总是对的。
sizeof的指针问题
考虑下面问题:
cout<<sizeof(string*)<<endl; // 4 cout<<sizeof(int*)<<endl; // 4 cout<<sizof(char****)<<endl; // 4可以看到,不管是什么类型的指针,大小都是4的,因为指针就是32位的物理地址。
结论:只要是指针,大小就是4(64位机变成8字节)。 但是其指向的变量占用不同大小的存储空间。
C++ STL vector:sizeof(vector)
向函数传递数组的问题
#include <iostream> using namespace std; int Sum(int i[]) { int sumofi = 0; for (int j = 0; j < sizeof(i)/sizeof(int); j++) //实际上,sizeof(i) = 4 { sumofi += i[j]; } return sumofi; } int main() { int allAges[6] = {21, 22, 22, 19, 34, 12}; cout<<Sum(allAges)<<endl; system("pause"); return 0; }Sum的本意是用sizeof得到数组的大小,然后求和。但是 实际上,传入自函数Sum的,只是一个int 类型的指针,所以sizeof(i)=4,而不是24,所以会产生错误的结果。解决这个问题的方法使是用指针或者引用。
int Sum(int (*i)[6]) { int sumofi = 0; for (int j = 0; j < sizeof(*i)/sizeof(int); j++) //sizeof(*i) = 24 { sumofi += (*i)[j]; } return sumofi; } int main() { int allAges[] = {21, 22, 22, 19, 34, 12}; cout<<Sum(&allAges)<<endl; system("pause"); return 0; }在这个Sum里,i是一个指向i[6]类型的指针,注意,这里不能用int Sum(int (*i)[])声明函数,而是必须指明要传入的数组的大小,不然sizeof(*i)无法计算。但是在这种情况下,再通过sizeof来计算数组大小已经没有意义了,因为此时大小是指定为6的。
此外,对函数使用sizeof,在编译阶段会被函数的返回值类型取代。
struct的sizeof问题
例如,下面的结构各成员空间分配情况:
struct test { char x1; short x2; float x3; char x4; };结构的第一个成员x1,其偏移地址为0,占据了第1个字节。
第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。
结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。
在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。
因为对齐问题使结构体的sizeof变得比较复杂,看下面的例子:(默认对齐方式下)
struct s1 { char a; double b; int c; char d; }; struct s2 { char a; char b; int c; double d; }; cout<<sizeof(s1)<<endl; // 24 cout<<sizeof(s2)<<endl; // 16对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。
对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。
这里有个陷阱,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:
struct s1 { char a[8]; }; struct s2 { double d; }; struct s3 { s1 s; char a; }; struct s4 { s2 s; char a; }; cout<<sizeof(s1)<<endl; // 8 cout<<sizeof(s2)<<endl; // 8 cout<<sizeof(s3)<<endl; // 9 cout<<sizeof(s4)<<endl; // 16;s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。
所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。
-------------------------------------------- 易错题 ----------------------------------------------
1. 用预处理指令#define声明一个常数,用以表明一年中有多少秒。
这是一个简单宏定义,考察三点:
(1)#define不能以分号结尾,括号使用。
(2)预处理器将为你计算常数表达式的值,因此写入计算式子比计算结果重要。
(3)意识到16位机将有一个整形数溢出,因此使用长整形,因此用到长整形符号UL。
答案:#define SECORES_PRE_YEAR (60*60*24*365) UL
2. 写一个标准宏,返回两个数较小那个。
这是一个带参数的宏,需要用到三元运算符。
答案:#define MIN(a,b) (a<=b ? a : b)
3. Const与define的区别?
二者都可以定义常量,区别在两点:
const常量有数据类型,宏常量没有数据类型。前者可进行安全检查,后者只进行字符替换,可能产生边际效应。
Const节省内存空间,存在编译器中,省去多余存取时间。
C++程序中Const常量完全取代宏常量。
C中默认const是外部连接,C迫使程序员使用#define。C++中默认const是内部连接,如果要定义外部连接,需要用extern const bufsize; //只声称
例如:const buffsize=100; char buf[bufsize]; 看起来是对的,但实际上buffsize占用了一部分内存,C编译器并不知道它在编译时候的值。可以选择这样写:
const buffsize;
内部连接和外部连接
4. sizeof与strlen的区别
sizeof度量内存空间大小,strlen度量char*指向的字符串长度而且必须以‘\0’结尾;
sizeof是运算符,strlen是函数;
当使用结构类型或者变量时,sizeof返回实际大小,使用静态空间时,返回全部数组的尺寸(计算结构变量大小必须考虑对齐问题);
sizeof不能用于函数类型、不完全类型(未知存储大小、void、未知内容)、或位字段。
例1: char ss[100]=“0123456789”;
sizeof(ss)结果为400;strlen(ss)结果为11。
例2:int ss[100]="0123456789";
sizeof(ss)结果为400;strlen(ss)错误,参数不是char类型。
例3:char* ss[100]=“0123456789”;//ss是指针
sizeof(ss)结果为4,ss是指针,指针在32位系统大小固定为4B。
sizeof(*ss)结果为1,ss指针指向的第一个字符。
例4:char ss[ ]=“0123456789”;//ss是数组
sizeof(ss)结果为11,ss是数组,计算到“\0”位置。
sizeof(*ss)结果为1,*ss是第一个字符。
5.sizeof用途:
与存储分配和I/O系统类似例程进行通信;
查看某种类型的对象在内存中所占的单元字节;
给出动态分配内存的大小;
便于类型扩充。
6. 内联函数和宏定义的区别.
(1)内联函数可以加快程序运行。因为不需要中断调用,而宏只是一个简单的替换。
(2)内联函数要做参数类型检查。
内联函数适用于:一个函数不断地被调用;函数只有简单的几行。