此系列为在学习牛客网C++面试宝典过程中记录的笔记,本篇记录第一章C/C++基础部分的第一节:语言基础。
牛客网C++面试宝典链接:https://www.nowcoder.com/issue/tutorial?tutorialId=93&uuid=a34ed23d58b84da3a707c70371f59c21
参考回答
C++在C语言基础上引入了面对对象的机制,同时也兼容C语言。
C++有三大特性(1)封装。(2)继承。(3)多态;
C++语言编写出的程序结构清晰、易于扩充,程序可读性好。
C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%;
C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等;
C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。
同时,C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。
参考回答
C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等。
C++是面对对象的编程语言;C语言是面对过程的编程语言。
C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库更灵活、更通用。
参考回答
struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;
struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的,例如:
struct A{ int iNum; // 默认访问控制权限是 public } class B{ int iNum; // 默认访问控制权限是 private }
在继承关系中,struct 默认是公有继承,而 class 是私有继承;
class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数,例如:
template<typename T, typename Y> // 可以把typename 换成 class int Func(const T& t, const Y& y) { //TODO }
答案解析
struct Student
{
int iAgeNum;
string strName;
} typedef struct Student Student2; //C中取别名
struct Student stu1; // C 中正常使用
Student2 stu2; // C 中通过取别名的使用
Student stu3; // C++ 中使用
(1)尖括号<>
的头文件是系统文件,双引号""
的头文件是自定义文件。
(2)编译器预处理阶段查找头文件的路径不一样。
(1)使用尖括号<>
的头文件的查找路径:编译器设置的头文件路径–>系统变量。
(2)使用双引号""
的头文件的查找路径:当前头文件目录–>编译器设置的头文件路径–>系统变量。
参考回答
关键字:在C++中,导入C函数的关键字是extern
,表达形式为extern “C”
, extern "C"
的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
编译区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型
也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
答案解析
//extern示例
//在C++程序里边声明该函数,会指示编译器这部分代码按C语言的进行编译
extern "C" int strcmp(const char *s1, const char *s2);
//在C++程序里边声明该函数
extern "C"{ #include <string.h>//string.h里边包含了要调用的C函数的声明 }
//两种不同的语言,有着不同的编译规则,比如一个函数fun,可能C语言编译的时候为_fun,而C++则是__fun__
参考回答
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
答案解析
1、预编译:这个过程主要的处理操作如下:
(1) 将所有的#define删除,并且展开所有的宏定义
(2) 处理所有的条件预编译指令,如#if、#ifdef
(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
(4) 过滤所有的注释
(5) 添加行号和文件名标识。
2、编译:这个过程主要的处理操作如下:
(1) 词法分析:将源代码的字符序列分割成一系列的记号。
(2) 语法分析:对记号进行语法分析,产生语法树。
(3) 语义分析:判断表达式是否有意义。
(4) 代码优化:
(5) 目标代码生成:生成汇编代码。
(6) 目标代码优化:
3、汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
4、链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。
链接分为静态链接和动态链接。
静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。
而动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
参考回答
1、定义全局静态变量和局部静态变量:在变量前面加上static关键字。初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。只不过全局静态变量和局部静态变量的作用域不一样;
2、定义静态函数:在函数返回类型前加上static关键字,函数即被定义为静态函数。静态函数只能在本源文件中使用;
3、在变量类型前加上static关键字,变量即被定义为静态变量。静态变量只能在本源文件中使用;
//示例 static int a; static void func();
4、在c++中,static关键字可以用于定义类中的静态成员变量:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间。
5、在c++中,static关键字可以用于定义类中的静态成员函数:与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间。
答案解析
当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针(划重点,面试题常考)。既然它没有指向某一对象,也就无法对一个对象中的非静态成员进行访问。
参考回答
1、概念:
(1)数组:数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。
(2)指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。 指针名指向了内存的首地址。
2、区别:
(1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
(2)存储方式:
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上。
指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。
(3)求sizeof:
数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
(4)初始化:
// 数组
int a[5] = { 0 };
char b[] = "Hello"; // 按字符串初始化,大小为6
char c[] = { 'H','e','l','l','o','\0' }; // 按字符初始化
int* arr = new int[10]; // 动态创建一维数组
// 指针
// 指向对象的指针
int* p = new int(0); delete p;
// 指向数组的指针 int* p1 = new int[10]; delete[] p1;
// 指向类的指针: string* p2 = new string; delete p2;
// 指向指针的指针(二级指针) int** pp = &p; **pp = 10;
(5)指针操作:
数组名的指针操作
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组
p = a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][] //所以数组指针也
称指向一维数组的指针,亦称行指针。
//访问数组中第i行j列的一个元素,有几种操作方式:
*(p[i]+j)、
*(*(p+i)+j)、
(*(p+i))[j]、
p[i][j]。
其中,优先级:()>[]>*。 //这几种操作方式都是合法的。
指针变量的数据操作:
char *str = "hello,douya!";
str[2] = 'a';
*(str+2) = 'b';
//这两种操作方式都是合法的。
参考回答
概念:函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
定义形式如下:
int func(int a);
int (*f)(int a);
f = &func;
函数指针的应用场景:回调(callback)
。我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback。
答案解析
//以库函数qsort排序函数为例,它的原型如下:
void qsort(void *base,//void*类型,代表原始数组
size_t nmemb, //第二个是size_t类型,代表数据数量
size_t size, //第三个是size_t类型,代表单个数据占用空间大小
int(*compar)(const void *,const void *)//第四个参数是函数指针
);
//第四个参数告诉qsort,应该使用哪个函数来比较元素,即只要我们告诉qsort比较大小的规则,它就可以帮我们对任意数据类型的数组进行排序。在库函数qsort调用我们自定义的比较函数,这就是回调的应用。
//示例
int num[100];
int cmp_int(const void* _a , const void* _b){//参数格式固定
int* a = (int*)_a; //强制类型转换
int* b = (int*)_b;
return *a - *b;
}
qsort(num,100,sizeof(num[0]),cmp_int); //回调
参考回答
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
答案解析
1、作用域:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。
静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
类静态成员变量:类作用域。
2、所在空间:都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
3、生命周期:静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。类静态成员变量在静态存储区,当超出类作用域时回收内存。
参考回答
能。
原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。
答案解析
//给出实例
class animal{
public:
void sleep(){ cout << "animal sleep" << endl; }
void breathe(){ cout << "animal breathe haha" << endl; }
};
class fish :public animal{
public:
void breathe(){ cout << "fish bubble" << endl; }
};
int main(){
animal *pAn=nullptr;
pAn->breathe(); // 输出:animal breathe haha
fish *pFish = nullptr;
pFish->breathe(); // 输出:fish bubble
return 0;
}
原因:因为在编译时对象
就绑定了函数地址
,和指针空不空没关系。pAn->breathe();编译的时候,函数的地址就和指针pAn绑定了;调用breath(*this), this就等于pAn。由于函数中没有需要解引用this的地方,所以函数运行不会出错,但是若用到this,因为this=nullptr,运行出错。
参考回答
1、概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2、产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
3、避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针
答案解析
产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。如:
char *p = (char *)malloc(sizeof(char)*100);
strcpy(p, "Douya");
free(p);//p所指向的内存被释放,但是p所指的地址仍然不变
...
if (p != NULL){//没有起到防错作用
strcpy(p, "hello, Douya!");//出错
}
避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
int *p = NULL; //初始化置NULL
p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间
assert(p != NULL); //判空,防错设计
p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址
free(p);
p = NULL; //释放后置空
int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0
assert(p1 != NULL); //判空,防错设计
free(p1);
p1 = NULL; //释放后置空
int *p2 = NULL; //初始化置NULL
p2 = new int[n]; //申请n个int内存空间
assert(p2 != NULL); //判空,防错设计
delete []p2;
p2 = nullptr; //释放后置空
参考回答
1、首先从作用域考虑:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。
全局变量:全局作用域,可以通过extern作用于其他非定义的源文件。
静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
2、从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
3、生命周期: 局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。
4、使用场景:从它们各自特点就可以看出各自的应用场景,不再赘述。
参考回答
区别:
1、宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
2、宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
3、宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等
答案解析
//宏定义示例
#define MAX(a, b) ((a)>(b)?(a):(b))
MAX(a, "Hello"); //错误地比较int和字符串,没有参数类型检查
//内联函数示例
#include
inline int add(int a, int b) {
return (a + b);
}
int main(void) {
int a;
a = add(1, 2);
printf("a+b=%d\n", a);
return 0;
}
//以上a = add(1, 2);处在编译时将被展开为:a = (a + b);
1、使用时的一些注意事项:
使用宏定义一定要注意错误情况的出现,比如宏定义函数没有类型检查,可能传进来任意类型,从而带来错误,如举例。还有就是括号的使用,宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性
inline函数一般用于比较小的,频繁调用的函数,这样可以减少函数调用带来的开销。只需要在函数返回类型前加上关键字inline,即可将函数指定为inline函数。
同其它函数不同的是,最好将inline函数定义在头文件,而不仅仅是声明,因为编译器在处理inline函数时,需要在调用点内联展开该函数,所以仅需要函数声明是不够的。
2、内联函数使用的条件:
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率 的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。
1.16 说说运算符i++和++i的区别
参考回答
先看到实现代码:
#include
int main(){
int i = 2;
int j = 2;
j += i++; //先赋值后加
printf("i= %d, j= %d\n",i, j); //i= 3, j= 4
i = 2;
j = 2;
j += ++i; //先加后赋值
printf("i= %d, j= %d",i, j); //i= 3, j= 5
}
1、赋值顺序不同:++ i 是先加后赋值;i ++ 是先赋值后加;++i和i++都是分两步完成的。
2、效率不同:后置++执行速度比前置的慢。
3、i++ 不能作为左值,而++i 可以:
int i = 0;
int* p1 = &(++i);//正确
// int* p2 = &(i++);//错误
++i = 1;//正确
// i++ = 1;//错误
4、两者都不是原子操作。
参考回答
1、new是操作符,而malloc是函数。
2、new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
3、malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
4、new可以被重载;malloc不行
5、new分配内存更直接和安全。
6、new发生错误抛出异常,malloc返回null
答案解析
malloc底层实现:当开辟的空间小于 128K 时,调用 brk()
函数;当开辟的空间大于 128K 时,调用mmap()
。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
new底层实现:关键字new在调用构造函数的时候实际上进行了如下的几个步骤:
1、创建一个新的对象
2、将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
3、执行构造函数中的代码(为这个新对象添加属性)
4、返回新对象
参考回答
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
1、const生效于编译的阶段;define生效于预处理阶段。
2、const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
3、const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
参考回答
1、定义不同 指针函数本质是一个函数,其返回值为指针。 函数指针本质是一个指针,其指向一个函数。
2、写法不同
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);
3、用法不同
用法参考答案解析
答案解析
//指针函数示例
typedef struct _Data{
int a;
int b;
}Data;
//指针函数
Data* f(int a,int b){
Data * data = new Data;
//...
return data;
}
int main(){
//调用指针函数
Data * myData = f(4,5);
//Data * myData = static_cast(f(4,5));
//...
}
//函数指针示例
int add(int x,int y){
return x+y;
}
//函数指针
int (*fun)(int x,int y);
//赋值
fun = add;
//调用
cout << "(*fun)(1,2) = " << (*fun)(1,2) ;
//输出结果
//(*fun)(1,2) = 3
参考回答
1. const int a; //指的是a是一个常量,不允许修改。
2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变
3. int const *a; //同const int *a;
4. int *const a; //a指针所指向的内存地址不变,即a不变
5. const int *const a; //都不变,即(*a)不变,a也不变
参考回答
1、定义指针时,先初始化为NULL。
2、用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
3、不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
4、避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
5、动态内存的申请与释放必须配对,防止内存泄漏
6、用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
答案解析
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
int *p = NULL; //初始化置NULL
p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间
assert(p != NULL); //判空,防错设计
p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址
free(p);
p = NULL; //释放后置空
int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0
assert(p1 != NULL); //判空,防错设计
free(p1);
p1 = NULL; //释放后置空
int *p2 = NULL; //初始化置NULL
p2 = new int[n]; //申请n个int内存空间
assert(p2 != NULL); //判空,防错设计
delete []p2;
p2 = nullptr; //释放后置空
参考回答
1、内联函数比普通函数多了关键字inline
2、内联函数避免了函数调用的开销;普通函数有调用的开销
3、普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
4、内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
答案解析
在使用内联函数时,应注意如下几点:
1、在内联函数内不允许用循环语句和开关语句。 如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。
2、内联函数的定义必须出现在内联函数第一次被调用之前。
参考回答
传参方式有这三种:值传递
、引用传递
、指针传递
1、值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
2、引用传递:形参在函数体内值发生变化,会影响实参的值;
3、指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
答案解析
值传递用于对象时,整个对象会拷贝一个副本,这样效率低;而引用传递用于对象时,不发生拷贝行为,只是绑定对象,更高效;指针传递同理,但不如引用传递安全。
代码示例
//代码示例
#include
using namespace std;
void testfunc(int a, int *b, int &c){//形参a值发生了改变,但是没有影响实参i的值;但形参*b、c的值发生了改变,影响到了实参*j、k的值
a += 1;
(*b) += 1;
c += 1;
printf("a= %d, b= %d, c= %d\n",a,*b,c);//a= 2, b= 2, c= 2
}
int main(){
int i = 1;
int a = 1;
int *j = &a;
int k = 1;
testfunc(i, j, k);
printf("i= %d, j= %d, k= %d\n",i,*j,k);//i= 1, j= 2, k= 2
return 0;
}
参考回答
//const* 是常量指针,*const 是指针常量
int const *a; //a指针所指向的内存里的值不变,即(*a)不变
int *const a; //a指针所指向的内存地址不变,即a不变