C/C++复习之路——王道程序员求职宝典——第五章《C预处理器、作用域、static、const以及内存管理》

C预处理器

  C预处理器处理程序的源代码,在编译器之前运行,通常以符号“#”开头。C语言预处理器主要有三个方面的内容:

  • 宏定义与宏替换;
  • 文件包含;
  • 条件编译。

宏替换

1)符号常量的宏定义和宏替换,格式如下:

#define 标识符 字符串

2)带有参数的宏定义及其替换,格式如下:

#define 标识符(参数列表) 字符串

例。如果有宏定义:

#define FUN(x) (x*x)

那么FUN(a+B)将被宏替换成_____?
解析:替换成(a+B)*(a+B)。可见如果我们的目的是(a+B)*(a+B),那么正确的宏定义是:

#define FUN(r) ((r)*(r))

可见,为了避免宏替换时发生错误,宏定义中参数应加上括号。

例。有如下一段代码:

#define ADD(x, y) x+y
int m=3;
m += m*ADD(m, m);

则m的值为()。
解析:15。m += m*ADD(m, m);会被替换成m += m*m+m;,因此结果为15。

宏替换的本质很简单——文本替换。

  1. 宏名一般用大写,宏名和参数的括号之间不能用空格,宏定义末尾不加分号。
  2. 宏替换只做替换,不做语法检查,不做计算,不做表达式求解。
  3. 宏替换在编译前进行,不分配内存,函数调用在编译后程序运行时进行,并分配内存。
  4. 函数只有一个返回值,利用宏则可以设法得到多个值。
  5. 宏替换使源程序变长,函数调用则不会。
  6. 宏替换不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)。
  7. 应尽量少用宏替换。在C++中宏替换实现的符号常量功能由const、enum代替,带参数的宏替换可有模板内联函数代替。

文件包含

  #include接受以下两种形式:

  • #include <standard_header> 认为头文件是标准头文件。编译器将会在预定义的位置集合中查找该头文件,这些预定义的位置可有通过设置查找路径环境变量或通过命令选项来修改。使用查找方法因编译器的不同而差别迥异。
  • #include “my_file.h” 认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。

条件编译

  格式如下:

#if/ifdef/ifndef
#elif
#else
#endif

注意事项:

  1. 预编译其变量经常用全部大写表示。
  2. 预处理器变量有两种状态:已定义或未定义。#define指示接受一个名字并定义改名字为预处理器变量。可以使用如下设施来预防多次包含同一头文件。
    #ifndef SALEITEM_H
    #define SALEITEM_H
    #endif
    

全局变量与局部变量

  在文件2.cpp中调用文件1.cpp中声明的全局变量,操作如下:
文件1:

int counter;

文件2:

extern int counter;
++counter;

  如果引用头文件的方式来引用某个在头文件中声明的全局变量,嘉定你将这个变量写错了,在编译期间会报错。假定你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,但是在连接期间报错。

局部变量

例。下属代码的执行情况是()。

int i = 1;
int main(int argo, char *argv[]){
    int i=i;
    cout<<i<<endl;
    return 0;
}
  • A. main函数里的i值未定义
  • B. main里的i值为1
  • C. 编译错误
  • D. main里的i值为0
    解析:A。int i=i。i变量从生命的那一刻开始就是可见的了,局部变量会屏蔽全局变量,因此main函数里面的i跟全局变量i无关,不是1,而是一个未定义的值。
    PS:我在自己电脑上运行,输出的结果是i=0,应该是编译器默认将没有赋值的变量都初始化为0了吧。

例。以下代码的输出结果为()。

int count = 3;
int main(int argo, char *argv[]){
    int i, sum, count=2;
    for (i=0,sum=0;i<count;i+=2,count++){
        static int count=4;
        count++;
        if(i%2==0){
            extern int count;
            count++;
            sum += count;//语句 1
        }
        sum += count;//语句2
    }
    printf("%d %d\n", count, sum);
    return 0;
}

解析:4 20。注意,题目中有三个count,分别是全局count,main里面的count,for里面的静态变量count。for循环条件中的count是mian里面的count,for循环里面的count是静态变量count,if语句里面的count是全局变量count,语句2里面的count是静态变量count,printf里面的count是main里面的count。若将if循环中的语句替换如下,答案也是相同的:

if(i%2==0){
	//extern int count;
	::count++;
	sum += ::count;
}

static

  不考虑类,static的作用主要有三条:

  1. 隐藏
      当我们编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。例a.c和b.c两个文件中,a中未加static是全局变量和函数,在b.c中都可以看到和调用,为了避免命名冲突,可以添加上static前缀。
  2. 默认初始化为0
    例。下面数组的值是什么?
    string sa[10];	//也可以不加static,也可以初始化为0
    static int ia[10];	//空字符串
    int main(){
        string sa2[10];	//空字符串
        int ia2[10];	//函数体内定义的内置数组,各元素未初始化,其值不确定
    }
    
  3. 保持局部变量的持久性
      函数内定义的局部变量,在函数退出时消失,重新进入时局部变量重新赋值。使用static定义局部变量却可以始终存在,也就是说它到底生命周期是整个源程序,且只进行一次初始化且具有“记忆性”。简单来说,就是第一次调用搞定时候初始化,后面在调用就不会再次初始化,而在每次调用时对其的改变可以保留到下次再次调用时。
      退出函数后,尽管该变量还继续存在,但是不能使用它。static局部变量存储在BSS段(未初始化段)或数据(data)段中,普通局部变量存储在堆栈中。
    例。下面代码中,变量val的内存地址位于()。
    void func(){
    	static int val;
    }
    
    A.已初始化数据段 B.未初始化数据段 C.堆 D.栈
    解析:B。未初始化数据段即BSS段,val未初始化,故存放在BSS段中;若初始化过了,则应选A。

类中static的作用

  C++重用了static这个关键字,斌赋予了它与前面不同的含义,:表示属于一个类而不属于此类的任何特定对象的变量和函数。
  在类内数据的声明前加上关键字static,该数据成员就是类内的静态数据成员。通常,非static数据成员在于类类型的每个对象中。不像普通数据成员,static数据成员独立于该类的任意对象而存在;每个static数据成员是与类关联的对象,并不与该类的对象相关联,也就是说某个类的实例修改了该静态成员变量,其修改值为该类其他所有实例所见。
  静态数据成员和普通数据成员一样遵从public、protected、private访问规则。
  静态数据成员也存储在全局(静态)存储区,静态数据成员定义时要分配控件,所有不能在雷内声明中定义。static数据成员必须在类定义体的外部定义(正好一次)。如可以定义银行账户类Account的initRate:

double Account::interestRate = initRate();

下面这样初始化是错误的:

class Accouunt{
	static double interestRate = 0.03;	//错误
	...
}

这个规则的例外,const static数据成员可以在雷内定义体中进行初始化。

例。C++中关于对象成员内存分布的描述正确的是()。

  • A. 不管该类被产生多少个对象,静态成员变量永远只有一个实例,且在没有对象实例的情况下已经存在。
  • B, 费静态成员数据在类中的排列顺序将和其被声明的顺序相同,任何中间介入的静态成员都不会被放进对象的内存布局中。
  • C. 在同一访问段(也就是private,public,protected等区间段内),数据成员的排列符合“较晚出现的成员在对象中有较高的内存地址”。
  • D. 带有虚函数的类对象占用的内存大小跟虚函数的个数成正比。
    解析:ABC。

静态成员函数

  普通成员函数总是具体的属于某个类的具体对象,所以普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身。但是静态成员由于不与任何的对象相关联,因此不具有this指针。因为它无法访问类对象的费静态数据成员,也无法访问费静成员函数,它只能调用其余的静态成员函数与访问静态数据成员。
  static成员函数不是任何对象的组成部分,因此static成员函数不能声明const。毕竟,将成员函数声明为const后就承诺不会修改函数所属的对象,而static成员函数不属于任何对象。
  static成员函数也不能被声明为虚函数、volatile。

总结下:

  • 非静态可以直接访问静态
  • 静态不可以直接访问非静态
  • 因为静态函数可以直接通过 类::函数 中方式调用,不用通过对象来调用函数,而非静态数必须通过对象来调用,这里面还涉及到实例化对象时候的内存分配。

const

  C++中,const限定符把一个对象转换成一个常量,incident在定义时必须初始化:

const int i,j=0;	//错误
const int bufSize = 512;

  在全局作用域里定义非const变量时,它在整个程序中都可以访问。单const在全局域声明的变量是定义该对象的文件的局部变量,即不可以被其他文件访问。只有指定const变更为extern才可以让其他文件访问该变量:
file_1.cpp

extern const int counter=10;

file_2.cpp调用file_1.cpp中的const变量counter

extern const int counter;
...

const在C和C++中的区别
下面的语句在C语言中编译错误,因为在C中const意思是“一个不能被改变的普通变量”,即它被放在内存中,C编译器不知道它在编译时的值。但在C++中,下面的语句是可行的。

const bufSize = 100;
int buf[bufSize ];

const比#define有更多的优点:

  • const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
  • 使用const可能比#define产生更小的目标代码,这是因为预处理器“盲目地将宏名称BUFSIZE替换为其代替的值100”可能会导致目标代码出现多份100的备份,但是常量就不会出现这种情况。
  • const还可以执行常量折叠(常量折叠是在编译时间简单化常量表达式的一个过程,简单来说就是讲常量表达式计算求职,并用求得的值来代替表达式,放入常量表),也就是说,编译器在编译时可以通过必要的计算把一个复杂的常量表达式缩减成简单的。
  • 综上所述,在C++中,我们应该用const代替#define。

指针和const修饰符

  • 指向const的指针
    如果指针指向const对象,则不允许使用指针来改变其所指的const的值。C++语言强制要求指向const对象的指针也必须具有const特性:
    const double *cptr;
    
    从标识符开始,是这样读的:“cptr是一个指针,它指向一个const double”。这里不需要初始化,因为cptr可以指向任何东西(也就是说它不是个const),但是它所指的东西是不能被改变的、
  • const指针
    使指针本身成为一个const,此时必须把const标明的部分放在*号右边:
    double d=1.0;
    double * const cptr=&d;
    
    它读作:“cptr是一个指针,这个指针是指向double的const指针”。因为现在指针本身是const,因此编译器要求给它一个初始化,这个值在指针寿命期间不变。然而要改变它所指向的值是可以的,写作:*cptr=2.0;
      可以使用下面两种合法形式中任何一种形式吧一个const指针变为指向一个const对象:
    double d=1;
    const double* const x=&d;	//语句1
    double const* const x2=&d;	//语句2
    

例。解释下面ptr的含义和不同。

  • double* ptr = &value;
    解析:ptr是一个指向double的指针,ptr的值可以变,ptr指向的value的值也可以变。
  • const double* ptr=&value;
    解析:ptr是一个指向const double的指针,ptr的值可以变,但是不能通过ptr修改value的值。
  • double* const ptr=&value;
    解析:ptr是一个指向double的const指针,ptr的值不可以变,但是可以通过ptr修改value的值。
  • const double* const ptr=&value;
    解析:ptr是一个指向const double的const指针,ptr的值不可以变,也不能通过ptr修改value的值。

例。下面的变量*p和它指向的字符串分别房子()。

int main(){
	char *p="hello, world";
	return 0;
}

A.堆和常量区 B.栈和栈 C.栈和常量区 D.栈和堆
解析:C。"hello,word"存放在文字常量区,变量p放在栈上。

例。请问运行下面函数会有什么样的结果?

char *GetMemory(void){
	char p[]="hello world";
	return p;
}
void Test(void){
	char *str=NULL;
	str = GetMemory();
	printf(str);
}

解析:结果可能是乱码。p是一个数组,其内存分配在栈上,故GetMemory返回的是指向“站内存”的指针,该指针的地址不是NULL,但其原来的内容已经被清楚,新的内容不知。可以做如下修改:

//(1)
char *GetMemory(void){
	char *p="hello world";	//p是指向全局(静态)存储区的指针,可以通过函数返回
	return p;
}
//(2)
char *GetMemory(void){
	static char p[]="hello world";	//数组位于静态存储区,可以通过函数返回
	return p;
}
//(3)
char *GetMemory(void){
	char *p=(char*)malloc(12);//p是指向堆栈中分配存储空间的指针,可以通过函数返回,但需要在以后条用delete[]释放内存,否则会造成内存泄漏
	if(p=NULL)
		return NULL;
	else
		return p="hello world";	
	return p;
}

例。以下哪些选项能编译通过()。

int i;
char a[10];
string f();
string g(string &str);

A.if(!!i){f();}; B.g(f()); C.a=a+1; D.g(“abc”);
解析:A。gcc下练市变量都作为常量,故f()的返回值作为临时常量(常量)返回给函数g时出错,因为常量不能传递给一个非常量引用。
C中a是常量,不可进行赋值运算。
D中通过类string的string (const char*)构造函数生成了一个临时变量,是常量,不能传递给非常量引用。若string g(string &str);修改为string g(const string &str);,则正确。

const数据成员

常量数据成员(常量成员变量)必须在构造函数的成员初始化列表中进行初始化。例如:

class Test{
	public:
		Test():a(0){}
		enum {size1=100,size2=200};
	private:
		const int a;	//只能在构造函数初始化列表中初始化
		static int b;	//在类的实现稳健中定义并初始化
		const static int c;	//与static const int c;相同,c为整型,故也可以在此处初始化,但仍需要在类定义体外进行定义,主要c为非整形时,不能在此处初始化,整形包括char、short、int、long
};
int Test::b=0;	\\成员变量不能在构造函数初始化列表中初始化,因为它不属于任何对象。
const int Test::c=0;	\\const static初始化时,不用添加static修饰,但是必须加const

例。将下面缺失的地方补全。

class Test{
	public:
		____ int a;
		____ int b;
	public:
		Test(int _a, int _b):a(_a) {b=_b;}
}int Test::b;
int  main(){
	Test t1(0,0), t2(1,1);
	t1.b=10;
	t2.b=20;
	printf("%u %u %u %u",t1.a, t1.b, t2.a, t2.b);
}

A.static/const B.const/static C.-/static D.const static/static E.None of the above.
解析:BC。观察输出,发现t1与t2的b是相同的,即仿佛“只有一份”,所以是静态的,答案便限定在BCD中,B正确,因为程序在胡世华后没有改变a的值,C也正确,默认是auto变量。D错误,a不是static(t1和t2的a值不相等),若是,还需要在类定义外定义。

内存与释放

  C/C++的程序,用户使用的内存主要分为以下几个部分:栈区(stack)、堆区、全局(静态)存储区、文字常量区、代码区(段)。

  • 栈区(stack),由编译器自动分配释放,存放函数的参数值局部变量的值等。其操作类似于数据结构中的栈,速度较快。例如函数中局部变量int b;便有系统自动在栈中开辟空间。
  • 堆区(heap),一般由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。注意它与数据结构中的堆是两回事儿,分配方式类似于链表。一般速度较慢,而且容易产生内存碎片,不过用起来方便。在C中使用malloc函数可以在堆上分配内存,例如:
     char * p1=(char*) malloc(10);	\\由free释放
    
    在C++中使用new运算符在堆上分配内存,如:
     char * p2=new char[10];	\\由delete[]释放
    
    但是注意p1和p2本身是在栈中的,但是它们指向堆上分配的内存。

例。以下分配的内存位于什么区域?

int a=0;	//全局初始化区
char *p1;	//全局未初始化区
void main(){
	int b;	//栈上
	char s[]="abc";	//s为一个大小为4的数组,存放在栈上
	char *p2;	//栈
	char *p3 ="123456";	//123456\0在文字常量区,p3在栈上,p3中存放指向文字常量区的地址
	static int c=0;	//全局(静态)初始化区
	p1=(char*)malloc(10);	//分配的1-和20字节存放在堆区,变量p1和p2位于栈上,p1和偏指向堆区分配的内存
	p2=(char*)malloc(20);
}
strcpy(p1,"12345");	//123456\0存放在常量区,编译器可能会把它与p3所指向的“123456”优化成同一个地方

C语言内存操作函数

例。下面程序的输出结果是()。

void GetMemory(char *p){
	p=(char*)malloc(11);
}
int main(){
	char *str="hello";
	GetMemory(str);
	strcpy(str, "hello word");
	printf("%s",str);
	return 0;
}

A.hello B.hello word C.hello worl D.Run time error/Core dump
解析:D。开始时,str是指向文字常量区的指针,GetMemory函数并不会为str新分配空间。
在这里插入图片描述
  如上图所示,函数调用传参时,str和形参的p虽然指向相同,但它们自身的地址不同,是两个不同的变量。
在这里插入图片描述
  如上图所示,p在执行malloc之后就指向不同的位置了,随后因为p是局部变量而被释放,malloc的空间没有free,成为无法引用的空间了。
  可见,str一直都是指向“hello”的,即str指向文字常量区,而文字常量是不允许修改的,故调用strcpy时会出错。

C++内存管理

  C语言使用malloc和free在自由存储区中分配存储空间,而C++则使用new和delete表达式实现相同的功能。
  动态创建对象如果不是显示初始化,那么对于类类型的对象,用该类默认构造函数初始化;而内置类型的对象则无法初始化,如:

string *ps=new string;	//调用默认构造函数初始化
int *pi=new int;	//无初始化

同样,也可以显式对动态创建的对象初始化:

string *ps=new string();	//调用默认构造函数初始化
int *pi=new int();	//初始化为0

malloc/free与new/delete的区别:

  • malloc/free是C/C++语言的标准库函数,new/delete是C++运算符
  • new自动计算需要分配的空间,而malloc需要手工计算字节数
  • new是类型安全的,而malloc则不是,比如
    int *p=new (float)[2];	//编译时出错
    int* p=(int*)malloc(2*sizeof(double));	//编译时无法指出错误
    
  • new调用operator new分配足够的空间,并调用相关对象的构造函数,而malloc不能调用构造函数;delete将调用实例的析构函数,然后调用operator delete,以释放该实例占用的控件,而free不能调用析构函数。
  • malloc/free需要库文件支持,new/delete不需要

例。下列程序中z的结果为()。

#define N 3
#define Y(n) ((N+1)*n)
z = 2*(N+Y(5+1));

解析:48。z=2*(3+((3+1)*5+1))=48。
例。以下表达式result的值是()。

#define VALL1(A,B) A*B
#define VALL2(A,B) A/B--
#define VALL3(A,B) ++A%B
int a=1;
int b=2;
int c=3;
int d=4;
int e=5;
int result=VALL2(a,b)/VALL1(e,b)+VALL3(c,d);

解析。B。result=a/b–/e*b+ ++c%d。注意,C预处理器在b+后面插入了一个空格。答案为1,运算结束后a为1,b为1,c为4,d为3,e为5。

例。下面的函数期望返回一个字符换,哪一个在运行时会出问题()。(多选)

  • A.
    char * GetString(){
    	char p[]="Good";
    	...
    	return p;
    }
    
    • B.
    char * GetString(){
    	char p[5]={'G','o','o','d','\0'};
    	...
    	return p;
    }
    
    • C.
    char * GetString(){
    	char *p="Good";
    	...
    	return p;
    }
    
    • D.
    char  * GetString(){
    	char *p=malloc(5);
    	if(p==NULL)
    		return NULL;
    	else
    		p="Good";
    	...
    	return p;
    }
    
    • E.
    char GetString(){
    	char *p="Good";
    	...
    	return *p;
    }
    
    • F.
    char GetString(){
    	char *p="Good";
    	...
    	return p;
    }
    
    解析:AB。A、B中的p数组都是临时的,存储在栈上,当程序调用结束后,数组所占的内存即被释放,根本传不到主函数里,运行时出错。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值