C++基础语法面试题
malloc/free和new/delete的区别
int *p;
p = (int*)malloc(sizeof(int) * 128);
//分配128个(可根据实际需要替换该数值)整型存储单元,
//并将这128个连续的整型存储单元的首地址存储到指针变量p中
int *parr;
parr = new int[100](0);
//返回类型为int *类型(整数型指针),分配大小为sizeof(int) * 100;
//初始化为0
共同点是:
都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间不会调用构造函数与析构函数,而new先申请空间后会调用构造函数完成对象的初始化,delete先调用析构函数再释放空间完成空间中资源的清理
- new/delete比malloc和free的效率稍微低点,因为new/delete的底层封装了malloc/free
C++的内存管理
C++:
- 代码区:存放函数体的二进制代码
- 常量区:存放字符串常量
- 全局/静态存储区: 存放全局变量和静态变量 (初始化和未初始化)
- 堆区:malloc申请分配的空间
- 自由存储区:new申请分配的空间(也是在堆的基础上)
- 栈区:局部变量,函数结束后自动释放
C语言:
静态区域:
- text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
- data segment(数据段):存储程序中**已初始化的全局变量和静态变量****
- bss segment:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0
动态区域:
- heap(堆): 当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。4G大小
- memory mapping segment(映射区):存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)
- stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。1M大小
如何避免内存泄漏
内存泄漏:动态分配的堆内存本应释放而未释放,造成可用空间减少。
- 不要手动管理内存,可以尝试适当使用智能指针
- 使用string而不是char*,string 类在内部处理所有内存管理,而且它速度快且优化得很好
- 使用了内存分配的函数,要记得使用其想用的函数释放掉内存。可以始终在new和delete之间编写代码,通过new关键字分配内存,通过delete关键字取消分配内存
- 培养良好的编码习惯,在涉及内存的程序段中,检測内存是否发生泄露。
堆与栈的区别
堆 | 栈 | |
---|---|---|
管理方式 | new,delete,手动 | 编译器自动管理 |
空间大小(32位) | 4G | 1M |
产生碎片 | 产生大量碎片 | 无碎片 |
生成方式 | 低地址–>高地址 | 高地址–>低地址 |
分配方式 | 动态分配 | 静态,动态(alloca,自动释放) |
分配效率 | 低 | 高 |
介绍一下C++智能指针
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
C++中类成员访问权限
c++通过public、protect、private三个关键字来控制成员变量和成员函数的访问权限,即公有的、受保护的、私有的, 被称为成员访问限定符 。在类的内部,可以相互访问没有限制。在类的外部,publiic可以通过对象、友元函数、子类函数访问,protect可以通过友元函数、子类函数访问,private只能通过友元函数访问。
public:
- 继承访问控制:
- 基类的public和protected成员:访问属性在派生类中不变
- 基类的private成员:不可直接访问
- 访问权限
- 派生类函数成员函数:可以直接访问基类中的public和protected成员,但不可以访问private成员
- 通过派生类的对象:只能访问public成员
private:
- 继承访问控制:
- 基类的public和protected成员:访问属性在派生类中以private身份出现
- 基类的private成员:不可直接访问
- 访问权限
- 派生类函数成员函数:可以直接访问基类中的public和protected成员,但不可以访问private成员
- 通过派生类的对象:不能直接访问基类继承的任何成员
protected:
- 继承访问控制:
- 基类的public和protected成员:访问属性在派生类中以protected身份出现
- 基类的private成员:不可直接访问
- 访问权限
- 派生类函数成员函数:可以直接访问基类中的public和protected成员,但不可以访问private成员
- 通过派生类的对象:不能直接访问基类继承的任何成员
- protected成员特点与作用
- 对建立其所在类对象的模块来说,它与private成员性质相同
- 对于其派生类来说,它与public成员的性质相同
static关键字的作用
全局静态变量:在全局变量前加一个static关键字
- 内存位置:全局/静态存储区,整个程序运行期间一直存在
- 作用域:在声明它的文件都是可见的,在文件之外不可见
- 未经初始化的全局静态变量会被自动初始化为0
局部静态变量:在局部变量前加一个static关键字
- 内存位置:全局/静态存储区
- 作用域:局部作用域,定义他的函数或者代码块,当作用域结束时,局部静态变量并没有销毁,继续在内存中,只不过不可以对其访问,当函数再次被调用,并且值不变
- 首次初始化,未经初始化的局部静态变量会被自动初始化为0,以后再调用不会进行初始化
静态函数:在函数前加一个static关键字
- 只在声明的文件中可见,不可被调用
- 在其他cpp文件中定义相同名字的函数,不会发生冲突
静态数据成员:在类内数据成员前加一个static关键字
- 静态数据成员被当成类成员,无论对象被定义多少个,静态数据成员只有一个,并且由所有对象共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性
- 存储在全局存储区,声明在类内,定义初始化必须要类外
- 可以通过对象名或者类名进行访问
静态成员函数:
-
属于类的静态成员
-
不具有this指针,静态成员函数无法访问非静态成员,只能调用静态成员; 非静态成员函数可以任意地访问静态成员;
-
可以通过类名和对象名进行访问
C++11与C++98的区别
- 使空指针(nullptr)取代NULL
- 智能指针,不需要手动释放空间
- 增加了一个MOVE的概念,就是可以将一个对象的东西全部给另一个对象,然后自己清空
- 可以使用auto
- Lambda函数和算法
C++与C的区别
C语言设计首先考虑是如何通过一个过程,对输入进行处理得到结果。
C++首先考虑如何构造一个对象模型,使用对象解决问题。
- C++面向对象,支持类、封装、继承、多态,C面向过程
- C++可以重载函数,C不可以重载
- C++和C动态管理内存不一样,malloc和new
- C++的类是C中没有的,结构体的成员默认是public,而类的是private
- C++中有引用,而C没有
面向对象与面向过程的区别?
面向过程是一种以过程为中心的编程思想,以算法进行驱动,程序=算法➕数据
面向对象是一种以对象为中心的编程思想,以消息进行驱动,程序=对象➕消息
C++中类与C中的结构体的区别(class,struct)
C++中的struct可以创建成员函数,而C中的struct不可创建成员函数
C++中struct可以有继承,虚函数,多态,而C中struct不可以。
C++中class中默认访问类型是private,而struct默认是public。
C++中class可以定义模板参数,但struct不可以。
struct可用来定义简单数据结构,class用来定义复杂对象
C++所有动作都是由main()引起的?如果不是,请举例
不是,如全局变量、静态变量的初始化,异常中断、内联、宏定义等
C++程序中调用c语言函数
引入c库文件include"a.h" 或者 extern"c"{ 函数名}
c++支持函数重载,在编译生成的汇编码中,要对函数的名字进行一些处理,比如加入函数返回类型等等
而c不支持函数重载,只是简单的函数名
函数被c++编译后在库中的名字与c语言不同,所以c++程序无法直接调用c函数
内联函数与宏定义的区别?
-
宏不是函数,只是在编译前将程序中相关字符串替换成宏体;内联函数是函数,在编译时不产生单独的代码,而是将相关代码嵌入到调用处,减少类普通函数调用时的资源消耗
-
宏没有返回值,不需要做参数类型检查;内联函数有返回值,需要做类型参数检查,比较安全
-
在类中声明同时定义的成员函数自动转化为内联函数(inline必须连着函数体)
-
内联函数适用范围
- 函数不断被重复调用
- 函数只有简单的几行,且不包括for while switch语句
虚函数可以是内联函数,但是当虚函数表现多态性的时候不能内联
因为内联是编译的时候确定的,而多态是在运行时,编译器无法知道运行期间调用哪个代码
指针和引用的区别?
- 指针需要分配自己的空间,而引用只是一个别名,不要分配内存空间
- 指针可以是空的,而引用必须是对一个对象,不可以为空
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被修改。
- sizeof指针,返回的是指针的大小,而sizeof引用返回的是对象的大小
- 指针可以多级,而引用只有一级
- 引用无需解引用,指针需要解引用
野指针
定义:指针指向一块随机的空间,不受程序控制
原因:
- 指针定义的时候未初始化:他会随机指向一个区域
- 指针被释放时没有置空,指针指向的空间被释放后,如果程序员没有对其进行置空或者其他赋值操作,就会成为一个野指针
- 指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放
规避方法:
- 初始化指针将其置为为NULL
- 释放指针的时候将其置为NULL
什么是构造函数与析构函数:
构造函数是在创建对象的时候进行调用,主要用来对对象成员变量进行初始化赋初值。一个对象可以有多个构造函数,可以重载构造函数(根据参数列表的不同)。
析构函数是在对象生命周期结束时释放对象所占用的资源。析构函数是特殊的类成员函数,他的名字与类名相同,没有返回值,没有参数不可以随意调用与重载,只由系统自动调用。
继承时,构造函数和析构函数的调用顺序
- 构造时,先调用父类的构造函数,再初始化成员,最后调用自己的构造函数
- 析构时,先调用自己的析构函数,再析构成员,最后调用父类的析构函数
一个类的成员变量的初始化时机取决于构造函数的实现,如果是在构造函数内部实现,那先调用构造函数,再初始化成员变量。如果是在成员初始化列表里初始化,那就是先初始化类成员,然后调用构造函数。
拷贝构造函数
拷贝构造函数是一特殊的构造函数,函数名称必须和类名一致,他的必须一个参数是本类型的一个引用变量。 X(const X& A){value = A.value;}
一个类中可以出现多于一个拷贝构造函数。
对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数;
使用引用是因为:避免拷贝构造函数无限制的递归下去;
调用时机:
- 对象以值传递的方式传入函数参数
- 对象以值传递的方式从函数返回
- 对象使用另一个对象初始化
深拷贝和浅拷贝
浅拷贝:默认的拷贝构造函数不会对对象中静态成员进行操作,对于需要动态开辟空间的指针只是做了简单的赋值,将指针指向了同一块内存,当析构时就对同一个空间释放两次,出现错误。
深拷贝:就是对对象中的动态成员重新分配空间,然后进行内容的赋值。
什么时候使用引用,什么时候使用指针?
1.使用引用参数的主要原因?
-
程序员能够修改调用函数中的数据对象
-
通过传递引用而不是整个数据对象,可以提高运行速度
2.对于使用传递的值而不作修改的函数:
如果数据对象很小,如内置数据对象,则按值传递
如果数据对象是数组,则使用指针,并将指针声明为指向const的指针
如果数据对象是较大的结构则使用const指针或const引用,以提高效率,节省复制结构所需要的时间和空间
如果数据对象是类对象则使用const引用。
3.对于修改调用函数中数据的函数:
如果数据对象是内置数据类型则使用指针
如果数据对象是数组则只能使用指针
如果数据对象是结构则使用引用或者指针
如果数据对象是类对象则使用引用
sizeof类型大小
长度 | 32位系统 | 64位系统 |
---|---|---|
char | 1 | 1 |
char* | 4 | 8 |
short | 2 | 2 |
int | 4 | 4 |
float | 4 | 4 |
long | 4 | 8 |
long long | 8 | 8 |
double | 8 | 8 |
随机数
#include<ctime>
#include<cstdlib>
srand(time(0)); //产生随机数种子,每次运行为不同结果,不加每次运行为同一结果
rang()%(b-a+1)+a;// [a,b]中的随机整数
string、char*、char[]之间转化
//char* s="alsd"的内容无法修改 char s[10]="aklsjd"的内容可以修改
//string --> const char* .c_str() .data()
string s="abcde";
const char *k = s.c_str();
const char *t = s.data()
//string --> char* .copy() 通过const char*转移
char* =<const_cast><char*>(const char*) //const char* 指针常量
string s="abcde";
int len = s.size();
char *a=new char[len+1]; //在堆上申请一个len+1大小的空间 因为还有'\0'结束符
//char *a = (char*)malloc((len+1)*sizeof(char));
s.copy(s,len,0);
//char* --> string 直接赋值
char *p = "asdhl";
string s = p;
//char* --> char[] strcpy()
char *p = "asdas";
char a[100];
strcpy(a,p);
//char[] -->char* 直接赋值
char a[]="akjsd";
char *p=a;
//char[] -->string 直接赋值
char p[]="asld";
string s=p;
//string --> char[] 数组赋值
int len =s.size();
for(int i=0;i<len;i++){
p[i]=s[i];
}
总结:
- 转string,直接赋值
- char[]转其他,直接赋值
- string 转 char* 通过const char*
sizeof与strlen的区别:
sizeof是运算符,strlen是函数。 数组做sizeof的参数不退化,传递给strlen就退化为指针了 。
//sizeof求的是字符串在内存的大小(包括'\0'),strlen求的是字符串到'\0'位置的大小(不包括'\0')
sizeof("abc") = 4;
strlen("abc") = 3;
char *a="qwerqwe";
sizeof(a) = 4; //指针大小
sizeof(*a) = 1; //*a指向'q',char的大小
strlen(a) = 7; //字符串长度
char b[10]="qwerqwe";
sizeof(b) = 10; //数组大小
sizeof(*b) = 1; //*b指向'q',char的大小
strlen(b) = 7; //字符串长度
size_t strlen(const char *str){
size_t len =0;
assert(str!=NULL);
while(*str++!='\0') len++;
return len;
}
数组取地址
int a[4]={1,2,3,4};
int *p = (int *)(&a+1);
cout<<*(a+1)<<*(p-1)<<endl;
输出 2,4;
因为a为数组名,a+1相当于&a[0]+sizeof(int)指向a[1]的地址;
&a+1相当于&a[0]+sizeof(4*int),数组末尾的后一个地址,p-1就指向了a[3];
C语言函数入栈顺序
从右往左入栈。栈底高地址,栈顶低地址,最先入栈的是函数调用处下条指令的地址,这样调用完成后返回到该地址继续执行,然后才是函数的入参。调用结束将栈内容出栈,最后栈顶指向开始指向了开始存储的指令地址。
int a=2;
cout<<a<<a++<<endl; //输出3,2,说明a++先入栈,a再入栈
cout<<++a<<a++<<a<<endl; //输出4,2,4,说明压栈时,a++会先记录a的值,而a与++a会先修改a的值,等待最终输出的时候才输出a的值
重写、重载、重定义
-
重写 override
定义:子类重写基类的虚函数
特点:(1)函数名相同 (2) 作用域不同(3) 参数列表相同(4)返回值相同
-
重载 overload
定义:在同一个作用域下根据参数来选择执行函数
特点:(1)函数名相同 (2)作用域相同 (3) 参数列表不同(4)返回值可以不同
-
重定义 overwrite
定义:子类重定义基类的虚函数
特点:(1)函数名相同 (2)作用域不同 (3)参数列表可以不同 (4) 返回值可以不同
**参数列表不同**:指的是个数或类型,但是不能靠返回类型来判断
多态
是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;在程序编译时多态性体现在函数和运算符的重载上;
动态绑定的条件是基类中的函数必须是虚函数,且派生类一定要重写基类的虚函数;第二点是通过基类类型的引用或者指针调用虚函数。
在基类函数前加上virtual,在派生类里面重写该函数,运行的时候根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
派生类指针不可以指向基类对象
虚函数
https://blog.csdn.net/qq_39755395/article/details/79751362
- 基类大小多出4个字节(虚函数指针,虚函数表中保存着所有虚函数的地址)
- 派生类继承基类,会继承基类的虚函数表中的元素
- 如果有重写,会覆盖虚函数表中同名基类函数的位置,非重写的虚函数会添加在表后
- 调用函数的时候会去虚函数表中找函数
哪些函数不可定义为虚函数?
- 友元函数,不是类成员函数
- 全局函数
- 静态成员函数,没有this指针
- 构造函数,拷贝构造函数
https://blog.csdn.net/ZongYinHu/article/details/51276806?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
构造函数和析构函数中调用虚函数不会产生多态
- 因为父类对象会在子类之前构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数是不安全的,此时会调用基类本身的虚函数
- 析构函数用来销毁一个对象,调用基类的析构函数时,派生类对象的数据成员已经销毁,这时候再调用虚函数也是基类本身的虚函数。
单继承:
每个类都有属于自己的虚函数表,派生类之间互不影响
类对象只存储指向虚函数表的指针vfptr,一个类的虚函数表只有一份,为所有对象共享
多继承:
派生类有几个基类就有该类几张虚函数表,派生类的虚函数表在最先声明的基类虚函数表中
如:C继承A,B,会有两个虚函数表指针,每个指针下先存放基类的成员变量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RzTYkSik-1591780187357)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1584189181398.png)]
1.pa与pc指向对象c的首地址,但pa和pb相差4字节
2.pa只能调用A中有声明的函数(func()被C覆盖),pb调用B中有声明的函数(func()被C覆盖),pc都可以调用
3.由于多态,访问C的func()
4.通过加作用域:pa->A::func();
在虚函数表中,子类覆盖了父类的虚函数表,可以从虚函数调用函数,也可以直接::限定作用域直接从A类的代码区寻找函数。
如果将pb强制转化成pa,pa->funcA()会根据偏移量自动回到pb->funcB();
5.在pa->funcC()无法调用,因为父类并没有这个函数,属于子类的成员函数,可以将其强制转换成C类型指针调用。
虚基类
- 解决多继承(派生类继承多个小基类,这些小基类又都是继承同一个大基类)时的二义性和冗余性问题
- 为最远的派生类提供唯一基类成员,而不重复产生多次复制
二义性也可以使用作用域限定符(d.Base::fun())
区别:虚基类只维护一份成员副本,而后者在派生类中拥有同名成员的多个副本,可以存放不同的数据。前者更加简洁,节省空间,后者可以容乃更多的数据。
在第一级继承时就要将共同基类设计为虚基类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-he35veQF-1591780187360)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1589090750810.png)]
虚函数和纯虚函数?
虚函数是根据一张虚函数表,每创建一个类对象, 根据对象实例的地址得到这张虚函数表 ,遍历得到其中的函数。
虚函数是为了让派生类可以重写这个函数,若派生类没有重写就是用基类的函数。
纯虚函数声明如下: virtual void Fun()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
为什么默认的析构函数不是虚函数?什么情况下析构函数应该声明为虚函数?
因为当类不作为基类时,如果将析构函数设置为虚函数,编译器就会给类增添一个虚函数表,里面面存放虚函数指针,会增加类的存储空间。
当基类的的指针指向派生类,并用基类的指针删除派生类对象时,当delete时,基类的指针指向派生类的析构函数,而派生类的析构函数又会调用基类的析构函数,这样整个派生类的对象完全被释放掉了。如果析构函数不是虚函数的话,编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数,这样就会造成派生类对象析构不完全。
引入头文件的时候,<>与""的区别
<>表示编译器会从系统配置的库环境中去寻找
""表示编译器先从当前项目的当前目录文件夹中寻找,找不到再去系统配置的库环境中去寻找
所以自己定义头文件需要使用"";
iostream iostream.h的区别
<iostream.h>继承c语言的标准库文件,未引入名字空间定义,里面的函数都是全局函数
是现在c++的规定,将标准c++库的组件放在一个名叫std的namespace里面
一个程序从开始运行到结束的完整过程(四个过程)
预处理—->编译—->汇编—->链接
预处理:编译器处理C程序的头文件,还有宏的替换等命令
编译:这个阶段编译器主要做词法分析、语法分析、语义分析等,在检查无错误后后,把代码翻译成汇编语言
汇编:汇编语言变成机器语言
链接:将编译阶段生成的文件以及他们所需要的库函数链接为一个可执行文件
静态链接 动态链接
https://blog.csdn.net/kang___xi/article/details/80210717
静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时。
静态链接:把要调用的函数或者过程直接链接到可执行文件中,成为可执行文件的一部分。换句话说,就是函数代码会在exe文件中,这个文件包括类运行所需要的全部代码。
缺点:(1)当多个程序调用相同函数时,每一个程序都会复制一次函数代码,浪费内存资源
(2)函数更新就需要重新进行编译链接可执行文件。
动态链接:仅仅在文件中加入了所调用函数的描述信息(重定位信息)。当应用程序被装进内存开始运行时,操作系统才将应用程序与动态链接库连接起来。当第一个程序程序使用了函数,第二个函数使用该函数不需要重新加载。
函数参数的默认值需要注意什么
**默认值设置:**可以在函数声明或函数定义中设置
**默认值位置:**指定默认值的参数必须位于形参的最右端,从右往左。第一个默认值后面的所有形参都必须指定默认值
**默认值的类型:**默认值可以是常量、全局变量里或者全局函数,不可以局部变量。因为函数参数默认值是在编译的时候确定的,而局部变量存放在栈中,编译时不能确定。
**参数是引用时的默认值:**全局变量可以作为引用参数默认值,不可以是实际值,因为引用必须引用一个对象
大端、小端的区别
11223344 | 低地址 | 高地址 | |||
---|---|---|---|---|---|
高尾端(大端) | 11 | 22 | 33 | 44 | |
低尾段(小端) | 44 | 33 | 22 | 11 |
bool IsBigEndian(){
union NUM{
int a;
char b[4];
}num;
num.a = 0x11223344;
if( num.b[3] == 0x11 ){
return true;
}
return false;
}