01 c++常见面试题总结
https://www.cnblogs.com/yjd_hycf_space/p/7495640.html C++常见的面试题
http://c.tedu.cn/workplace/217749.html C++开发需要掌握的技能
https://www.cnblogs.com/LUO77/p/5771237.html 常见C++面试题及基本知识点总结
1. 引用:
- 申明一个引用的时候,切记要对其进行初始化(引用只能在定义时指定,之后不能再次指向比的变量,即变成其他变量的引用)
- 本身不是一种数据类型,因此引用本身不占存储单元
- 数组的元素不能是引用。但是可以建立数组的引用。
解释:
2. 常量引用:
不能通过引用的别名改变值(不能通过引用对目标变量的值进行修改),但原变量是可以随意改变值的
3. 不能返回函数内部new分配的内存的引用
虽然new不存在局部变量的被动销毁问题,但是当被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
https://blog.csdn.net/why_ny/article/details/7901670
//编译可以通过,但是容易造成内存泄漏 string& foo() { string* str = new string("abc"); return *str; } //不会泄露 string& tmp = foo(); string str = tmp; delete &tmp; //临时变量时,无法释放 string str = "hello" + foo(); //调用foo()产生的临时变量,由于没有变量名,不能被释放
//无法释放
string str = foo(); //没有取引用,相当于返回一个临时的引用对象,然后该引用对象又向str赋值。即str与返回值并没有绑定
#include <iostream> using namespace std; int & ret(int &a){ a++; return a; } int main() { int a=1,aa=1; int b=ret(a); //返回一个临时引用变量,该临时变量又给b赋值 int &c=ret(aa); cout<<"b: "<<b<<endl; cout<<"c: "<<c<<endl; b=3; c=3; cout<<"b-a: "<<a<<endl; //b与a不相关 cout<<"c-aa: "<<aa<<endl; //c与aa相关 cout << "Hello World"<<endl; //cout<<1&1<<endl; return 0; } 输出: b: 2 c: 2 b-a: 2 c-aa: 3 Hello World
4. 关于运算符操作的引用
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。
赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
ostream &operator << (ostream &, class &)
1)返回值是引用 原因是为了多次<<,即cout<<a<<b<<"返回值"<<endl; 这样能够将a,b,”返回值”都存放在一个cout的缓冲区
如果返回值是对象,生成了另一个实例。相当于是将后续的内容放到了另一个ostream类实例中,而这个实例可能并未与标准输出设备连接,这样就没办法输出了
2)执行一次cout<<a并不一定直接将a输出到屏幕,而是将a转到IO的缓冲区,具体什么时候将a输出到屏幕要看系统什么时候有时间和c++的IO具体是怎么实现的。 系统掌握着消息队列,每次从里面抽取一条消息执行,因为CPU效率很高,我们感觉程序是连续运行的,其实不连续。对于多核处理器来说可以发生真实的同时执行几个程序。要强制系统将数据输出到屏幕上,可以“刷新”一下,比如用cout<<endl;就是打一个换行,再刷新缓冲区,刷到屏幕上。
在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。
5. struct与union的区别:
https://www.cnblogs.com/nktblog/p/4027107.html
例题:union类型是共享内存的,以size最大的结构作为自己的大小。所以注意其他size小的变量是怎么存储的
union myun { struct { int x; int y; int z; }u; int k; }a; int main() { a.u.x =4; a.u.y =5; a.u.z =6; a.k = 0; printf("%d %d %d\n",a.u.x,a.u.y,a.u.z); return 0; }
myun这个结构就包含u这个结构体,而大小也等于u这个结构体的大小,在内存中的排列为声明的顺序x,y,z从低到高,然后赋值的时候,在内存中,就是x的位置放置4,y的位置放置5,z的位置放置6,现在对k赋值,对k的赋值因为是union,要共享内存,所以从union的首地址开始放置,首地址开始的位置其实是x的位置,这样原来内存中x的位置就被k所赋的值代替了,就变为0了,这个时候要进行打印,就直接看内存里就行了,x的位置也就是k的位置是0,而 y,z的位置的值没有改变,所以应该是0,5,6。
例题: struct成员对齐
缺省情况下,编译器为结构体的每个 成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size最大的成员对齐。
struct naturalalign { char a; short b; char c; }; //在上述结构体中,size最大的是short,其长度为2字节,因而结构体中的char成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6; struct naturalalign { char a; int b; char c; }; //结果显然为12
指定对界
一般地,可以通过下面的方法来改变缺省的对界条件: --使用伪指令#pragma pack (n),编译器将按照n个字节对齐; --使用伪指令#pragma pack (),取消自定义字节对齐方式。 注意:如果#pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
#pragma pack (n) struct naturalalign { char a; int b; char c; }; 当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。
而当n为2时,其发挥了作用,使得sizeof(naturalalign)的结果为8
n=2时,int为4Byte,char向2字节对齐。 注意这里int不会变成2字节,是比n小的向n对齐
题目(微软)
#include <iostream.h> #pragma pack(8) struct example1 { short a; long b; }; struct example2 { char c; example1 struct1; short e; }; #pragma pack() int main(int argc, char* argv[]) { example2 struct2; cout << sizeof(example1) << endl; cout << sizeof(example2) << endl; cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl; return 0; } 输出: 8 16 (以结构1中的long对齐) 4
注:此处long取4字节(与系统有关)
6. C++不是类型安全的,两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
类型安全是指同一段内存在不同的地方,会被强制要求使用相同的办法来解释(内存中的数据是用类型来解释的)。
Java语言是类型安全的,除非强制类型转换。(也是不安全的)
C语言不是类型安全的,因为同一段内存可以用不同的数据类型来解释,比如1用int来解释就是1,用boolean来解释就是true。
7.main 函数执行以前,还会执行什么代码?
全局对象的构造函数会在main 函数之前执行。
8. 无论什么类型的指针(char、int)都是4字节(与系统有关,与类型无关)
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。 void Func(char a[100]) //尽管形参写的是数组,但实际上并不是数组,退化成指针 { cout<< sizeof(a) << endl; // 4 字节而不是100 字节 }
9. 函数指针
https://www.cnblogs.com/lvchaoshun/p/7806248.html
返回类型 (*指针名)(形参类型) 因为函数声明与返回类型和形参类型决定,与函数名没有关系
typedef decltype(函数名) 别名 定义了函数名函数的别名 使用时, 别名* 指针名。
typedef decltype(函数名)* 别名 定义了可指向函数名函数的函数指针
10. 基类非虚函数,继承类定义了同名的虚函数
#include <iostream> using namespace std; class A { public: void fuc() { printf("Hello fuc()\n"); } void fuc2() { printf("Hello A::fuc2()\n"); } }; class B:public A { public: virtual void fuc2() { printf("Hello B::fuc2()\n"); } }; int main() { B datab; A *PA=&datab; PA->fuc2(); //Hello A::fuc2() //cout << "Hello World"; return 0; }
输出:Hello A::fuc2()
PA->fuc2(); 不会发生动态绑定。会调用A类的函数。 若A类中 virtual void fuc2()也是虚函数,则运行时动态绑定,调用B类的函数。
11. 栈内存与文字常量区
char str1[] = "abc"; char str2[] = "abc"; const char str3[] = "abc"; const char str4[] = "abc"; //以下四个指针地址相同 const char *str5 = "abc"; const char *str6 = "abc"; char *str7 = "abc"; char *str8 = "abc"; cout << ( str1 == str2 ) << endl;//0 分别指向各自的栈内存 cout << ( str3 == str4 ) << endl;//0 分别指向各自的栈内存 cout << ( str5 == str6 ) << endl;//1指向文字常量区地址相同 cout << ( str7 == str8 ) << endl;//1指向文字常量区地址相同 结果是:0 0 1 1 解答:str1,str2,str3,str4是数组变量,它们有各自的内存空间;而str5,str6,str7,str8是指针,它们指向相同的常量区域。
因为对指针 str5,str6,str7,str8 来说,"abc" 属于常量字符串,不可改变。
12. 将程序跳转到指定内存地址
要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234; //用地址代替变量名。
让程序跳转到绝对地址是0x100000去执行:
将0x100000强制转换成函数指针,然后调用
*((void (*)( ))0x100000 ) ( ); 首先要将0x100000强制转换成函数指针,即: (void (*)())0x100000 然后再调用它: *((void (*)())0x100000)(); 用typedef可以看得更直观些: typedef void(*)() voidFuncPtr; *((voidFuncPtr)0x100000)();
13. 复杂声明
void * ( * (*fp1)(int))[10]; float (*(* fp2)(int,int,int))(int); int (* ( * fp3)())[10]();
【标准答案】
1.void * ( * (*fp1)(int))[10]; fp1是一个指针,指向一个函数,这个函数的参数为int型,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个void*型指针。
2.float (*(* fp2)(int,int,int))(int); fp2是一个指针,指向一个函数,这个函数的参数为3个int型,函数的返回值是一个指针,这个指针指向一个函数,这个函数的参数为int型,函数的返回值是float型。
3.int (* ( * fp3)())[10](); fp3是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是int型。
14. 析构函数
以下代码是否完全正确,执行可能得到的结果是____。 class A{ int i; }; class B{ A *p; public: B(){p=new A;} ~B(){delete p;} }; void sayHello(B b){ } int main(){ B b; sayHello(b); } A.程序正常运行 B.程序编译错误 C.程序崩溃 D.程序死循环
当类中存在指针类型的成员变量时赋值和析构要格外注意,这道题的问题就出在类B对象b中的指针p被析构了两次。
调用函数 sayHello(b),将b赋值给函数的形参,由于浅拷贝,两个对象的指针部分指向同一块内存。该函数结束,形参调用析构函数,被释放。而主函数结束后b也会调用析构函数,释放内存。但是该函数已经被释放了(形参释放)。因此同一块内存会被释放2次,报错。选择C。
注意,虽然main函数没有定义return语句,但是编译器会自动生成return语句。因此编译是正确的。
15. 常见数据类型及其长度:
- 注意long int和int一样是4byte,long double和double一样是8byte。(关于long double,ANSI C标准规定了double变量存储为 IEEE 64 位(8 个字节)浮点数值,但并未规定long double的确切精度。所以对于不同平台可能有不同的实现。有的是8字节,有的是10字节,有的是12字节或16字节。)
- 在64位操作系统下,int的长度还是32位的。
16. static的使用
- 对局部变量
当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。
- 全局变量
static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:
(1)不会被其他文件所访问,修改
(2)其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。
而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的.c文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。
-
类内数据成员
static数据成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化。可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型,即常量表达式。
static constexpr int period = 30;
1. 不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
2. 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
3. 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。
-
成员函数
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
不可以同时用const和static修饰成员函数。
1. C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。
2. const与指针:
const char *p 表示 指向的内容不能改变。
char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。
17. 指针和引用的区别
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(指针占用内存、引用不占用内存)
(2)可以有const指针,也有const引用(const引用可读不可改,与绑定对象是否为const无关)
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;
(7)指针和引用的自增(++)运算意义不一样;
指针传参的时候,还是值传递,试图修改传进来的指针的值是不可以的。只能修改地址所保存变量的值。 引用传参的时候,传进来的就是变量本身,因此可以被修改。
18. 隐藏
Overwrite(隐藏):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。
19. 虚函数表
第4部分内容
20. 深拷贝与浅拷贝:
浅拷贝:默认的复制构造函数只是完成了对象之间的位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝:自定义复制构造函数需要注意,对象之间发生复制,资源重新分配,即A有5个空间,B也应该有5个空间,而不是指向A的5个空间。
21. vector中size()和capacity()的区别
- size()指容器当前拥有的元素个数(对应的resize(size_type)会在容器尾添加或删除一些元素,来调整容器中实际的内容,使容器达到指定的大小。);
- capacity()指容器在必须分配存储空间之前可以存储的元素总数。
size表示的这个vector里容纳了多少个元素,capacity表示vector能够容纳多少元素,它们的不同是在于vector的size是2倍增长的。如果vector的大小不够了,比如现在的capacity是4,插入到第五个元素的时候,发现不够了,此时会给他重新分配8个空间,把原来的数据及新的数据复制到这个新分配的空间里。(会有迭代器失效的问题)
22. 各容器的特点:
注:
deque容器,双端队列,能够快速地随机访问任一个元素,并且能够高效地插入和删除容器的尾部元素。
23. map和set的原理
红黑树:
(深入探讨红黑树)