对于 王道程序员面试宝典 一书上的基础知识点总结,将陆续发表在各个专题,以方便今后查阅。
章节顺序并不是按时间顺序,而是根据自己的情况随机安排。
1. C预处理器在编译之前运行,通常以#开头,主要处理宏定义与宏替换,文件包含和条件编译。
2. 宏定义以#define开头,主要分为符号常量的宏定义和带参数的宏定义。
前者 表达式为 #define 标识符 字符串 后者表达式为 #define 标识符(参数列表) 字符串,而后者中为了为了避免替换时发生错误,宏定义的参数应该加上括号
3. #include文件包含包括两种形式
#include
#include "my_file.h" 表示该文件为非系统头文件,从源文件所在路径开始查找。
4.条件编译
作用是同一源文件程序可以根据不同的编译条件(参数)产生不同的目标代码,
其作用在于编译和调试。
利用条件编译避免多重包含:
#ifndef SALESITEM_H
#define SALESITEM_H
///此处为头文件包含
//此处是某个类的定义与相关函数的定义
#endif
其中#ifndef SALESITEM_H的作用是测试 SALESITEM_H预处理器变量是否未定义,如果未定义,则测试成功,跟在其后的所有行都将被执行,直至#endif。如果已定义,则忽略#ifndef和#endif指示间的代码。这样做的目的是保证头文件在给定的源文件中只处理一次。
5.全局变量与局部变量
全局变量的作用域为整个源文件,关键字extern可以在一个源文件中引用在另一个源文件中定义的全局变量。局部变量是指在特定过程或函数中使用的变量,可以和全局变量重名,并且会覆盖全局编。
6.static
不考虑类,static的作用主要有3个:
1)隐藏作用,当编译多个源文件时,所有未加static关键字的全局变量和函数都具有全局可变性,即对其它源文件也是可见的,而加了static关键字之后,对其它源文件就不可见了,这样可以防止命名冲突。
2)将未初始化的全局静态变量和局部静态变量初始化为0。
3)保持局部变量内容的持久性
7.static在类中的作用
C++重用了static关键字,并赋予其不同的含义,表示属于一个类而不是属于此类的任何特定对象的变量和函数。
静态数据成员:独立于该类的任意对象而只与该类相关联,也即当某个类的实例修改了该静态成员变量,其修改值为该类的其他所有实例所见。static数据成员必须在类定义体的外部定义,不能在类的定义体中初始化,static数据成员通常在类的定义体的外部定义时才能初始化,即在类定义体中对静态变量赋初值是错误的。此处有一个例外,基本整形const static 可以在类的定义体中进行初始化。
静态成员函数: 为类服务而不为某一个类的具体对象服务,普通成员函数总数属于该类的某个具体对象因此一般都隐藏了一个this指针指向该类的对象的本身,但静态成员函数不与类的任何对象相关联,故其不具有this指针。因而它无法访问类对象的非静态数据成员,也无法访问非静态成员函数,只能调用其余静态成员函数与访问静态数据成员,而非静态成员函数可以访问任意静态成员函数和静态数据成员。
static成员函数不能被声明为const,因为被声明为const就表示不会修改该函数所属的对象,而static成员函数不属于任何对象。最后,static成员函数也不能被声明为虚函数,volatile。
8.const常量
C++中const限定符将一个对象转换成一个常量,定义时必须初始化且不能被修改。
在全局定义域里声明的const变量,只存在于定义的变量中,而不能被其它文件访问,通过加extern关键字可以在整个程序中访问const队形。而在全局定义域里定义的非const对象在整个程序中都可以访问。
用const替代#define的值替换功能:
C语言中的值替换功能 #define BUFSIZE 100 而在C++中为const bufsize = 100 或者更清楚的形式
const int bufsize = 100; 这样做的优点主要有
1)const常量数据类型,编译器可以进行类型检查,而宏常量没有类型检查,只是单纯替换,并且在字符替换时可能会发生意料不到的错误。
2)使用常量可能比使用#define产生更小的目标代码。
3)const可以执行常量折叠,即常量表达式先计算求值,然后用所求值替换表达式,放入常量表。
9.指针和const修饰符
指向const对象的指针:
如 const double * cptr; (等价于 double const * cptr), 此处不需要初始化,因为cptr可以指向任何值,但是它指向的东西即 *cptr 是不能被改变的。(const直接修饰 *cptr)
const指针:
double d = 1.0;
double * const cptr = 2.0;
cptr是一个指针,这个指针是指向double的const指针,指针本身为const指针,故需要初始化,这个初始化值在指针寿命期间不变,但是其指向的值是可以改变的。(const直接修饰指针)
还有一种情况:
double d = 1;
const double * const x = &d;(等价于 double const * const x2 = &d;)
此时,指针和对象都不能改变。
10.const修饰函数参数与返回值
const修饰返回值:
const 修饰返回值常用在处理用户定义类型时。
当函数返回值为指针时,函数不能返回指向局部栈变量的指针,因为程序结束后栈会被清除,返回值无效。可返回指向堆中存储空间的指针或指向指向静态存储区的指针,在函数返回后它仍然有效。
例题2.请问运行test函数后会有什么样的结果?
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)
{
static char p[] = "hello world";//数组位于静态存储区,可通过函数返回。
return ;
}
2)char * GetMemory()
{
char *p = "hello world"; //p是指向全局(静态)存储区的指针,可通过函数返回
return p;
}
3) char * GetMemory(void)
{
char * p = (char*)malloc(12); //p是指向堆中分配存储空间的指针,可通过函数返回,
//但是需要以后调用delete[]释放内存,否则会造成内存泄露
if(p == NULL)
return NULLL;
else
{
p = "hello world";
return p;
}
}
const修饰函数的参数:
如果函数是指传递,可将形参限定为const(意义不大),即告诉编译器该值在函数内不会改变。
如果函数是传递至,则应尽可能将参数设定为const,否则将会使指向const的指针不能做实参。如:
int fun(int * i);
const int a = 1;
fun(&a);
编译错误,函数参数为int* 类型的形参,而&a为cont int * 类型的实参,类型不兼容。若函数声明为
int fun(const int * i); ,则int * 类型与 const int * 的实参都可以接受。 再如:
void f(int &i);
f(1);
调用f(1)产生编译错误,编译器为int类型实参分配临时存储单元将其初始化为1并为其产生一个地址和引用捆绑在一起,故实参是const int 类型,而形参是非const类型,故错误。
11.const在类中的应用
const成员函数:
class base
{
void func1();
void func2() const;
}
上述操作成员函数func2()后面的const改变了隐含的this形参的类型,使其指向的成员函数为const类型,而const函数不能修改 调用该函数的对象(mutable成员除外)。
const成员函数的目的,是为了确保该成员函数作用于const对象上。
const对象、指向const对象的指针只能调用其const成员函数而不能调用非const成员函数,否则会报错。而非const对象可调用非const成员函数和const成员函数,由此可见从const成员函数可以实现对const对象操作。const成员函数与非const成员函数之间可以实现重载操作。
const数据成员:
常量数据成员必须在构造函数的成员初始化列表中被初始化(必须有构造函数),当常量数据成员同时被声明为static时,可使用外部初始化。
例3:Fill the blanks inside class defination.
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);
}
running results: 0 20 1 20
解答:观察输出,可知t1与t2的b是相同的,仿佛"只有一份",所以是静态的,故第二个空填static.因为程序在初始化后没有改变a的值,故第一个空填const,不填也正确,即默认为auto变量。
12.内存分区
C/C++ 语言中,用户使用的内存主要分为 堆区,栈区,全局(静态)区,文字常量区,代码区。
堆:由程序员手动分配和释放,不同于数据结构中的堆,分配方式类似于链表。由malloc(C语言)或new(C++)分配,free(C语言)和delete释放。若程序员不释放,程序结束时由系统自动释放。
C语言用malloc函数在堆上分配内存,如 char * p1 = (char*)malloc(10)//由free释放。
C++ 用new运算符在堆上分配内存,如 char * p2 = new char[10]; //由delete[] 释放
栈:由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。
全局(静态)存储区:存放全局变量和静态变量。包括DATA段(全局初始化区,存放初始化的全局变量和
静态变量)和BSS段(全局未初始化区,存放未初始化的全局变量和未初始化的静态变量,在程序执行之前会自动清0)。程序结束后由系统释放。
文字常量区:存放常量字符串,程序结束后自动释放。
程序代码区:存放函数体的二进制代码。
13.C语言使用一对标准库函数malloc和free在自由存储区分配存储空间,C++则使用new和delete表达式实现
同样的功能。
14.C++ 内存管理
动态创建对象的初始化: 对于动态创建的对象,可显式对其进行初始化,如string * ps = new string();但是往往没有这个必要,因为无论程序明确不初始化还是要求进行初始化,都会自动调用其默认构造函数初始化该对象,如:string * p = new string; 二者没有差别。
而对于内置类型或没有定义默认构造函数的类型,采用不同初始化方式则有显著差别:
int *pi = new int; int * pi = new int();
第一个语句int型变量没有初始化,第二个语句int型变量则被初始化为0;
动态创建的对象用完后,必须显式的释放其所占用的内存,C++ 提供了delete表达式完成此工作。
回收new分配的单个对象的内存时,用delete,回收用new[]分配的一组对象的内存空间时,用delete[];
15.const 对象的动态分配与回收
C++允许动态创建const对象,如
const int * pci = new const int(1024); 与其他常量一样,动态创建的const必须在创建时初始化,并且初始化之后的值不可在修改。由于new返回的对象存放const对象,因此该地址只能赋值给const的指针。
对于类类型的const动态对象,如果该类提供了默认构造函数,则此对象可隐式初始化,如
const string * pcs = new const string;
new 表达式没有显示初始化pcs 所指向的对象,而是隐式将pcs所指对象初始化为空的string对象。
内置类型的对象或未提供默认构造函数的类类型对象必须显式初始化。
const动态对象也是使用删除指针来释放的:如:delete pci;
16.malloc/free对象与new/delete 对象的区别
相同点:都可以用于申请动态内存和释放内存;
不同点:
1)操作对象不同
malloc/free是C/C++语言的标准库函数而不是运算符,故不在编译器控制范围内,无法执行构造函数和析构函数。
new/delete 是C++d 运算符。
new的执行过程为:首先调用 operator new 标准库函数,分配一个足够大的原始的未初始化内存,以保存一个指定类型的对象;然后运行该类型的一个构造函数,用指定初始化方式构造对象;最后返回新分配并构造的对象的指针。
delete的执行过程:首先对sp指向的对象运行适当的析构函数;然后调用operator delete 的标准库函数释放该对象所用内存。
2)用法不同
malloc返回值类型是 void*,所以在调用malloc时要显示的进行类型转换,将void* 转换成所需要的类型指针,malloc函数本身只关心内存的总字节数而并不识别要申请的内存时什么类型。
调用free(p)释放内存,如果p是NULL指针,则free对p无论释放多少次都不会出现问题,而如果p不是NULL指针,则free对p联系释放两次就好出现运行错误。
而new内置了sizeof,类型转换和类型安全检查功能,对于非内置类型对象而言new在动态创建时即完成了初始化工作(调用构造函数)。