1. 引用和指针的区别?
- (内存空间分配)指针是一个实体,需要分配内存空间,而引用只是变量的别名,不需要分配内存空间。
- (初始化和指向空间)引用定义时必须要初始化,并且不能改变指向空间大小,而指针定义时不需要初始化,可以改变指向空间,因此,不存在指向空值的引用,但存在指向空值的指针。
- (多级)存在多级指针,但不存在多级引用,只有一级引用
- (自增)指针自增和引用自增不同,指针自增指向下一个空间地址,而引用自增是使所对应的实参变量进行+1。
- (sizeof)sizeof引用计算的是实参对象本身的大小,而sizeof指针是计算指针本身的大小。
- (访问方式)引用访问一个变量是直接访问,而指针访问一个变量是间接访问。
- (指针类型检测)使用指针前最好进行类型检查,避免使用野指针。
- (引用底层)引用底层是通过指针进行实现的。
- (参数传递)作为参数传递时,指针传递的实质是值传递,传递的是地址值,而引用传递的实质是地址传递,传递的是变量的地址。
2. C++中的指针参数传递和引用参数传递
- 指针参数传递的实质是值传递,传递的是地址值。值传递过程中,当通过主调函数调用被调函数时,会在栈中开辟一块内存空间,存储实参传递的值,也就形成了一个实参的副本–形参。值传递的特点是,无论形参发生什么改变,都不会影响实参的变化。
- 引用参数传递的实质是地址传递,传递的是变量的地址。地址传递过程中,主调函数调用被调函数时,在栈中开辟一块内存空间,存储实参变量的地址。被调函数对形参的任何操作都会被会被处理成间接寻址,即通过栈中存放的变量地址访问变量主调函数中的实参变量。因此,被调函数对形参的任何操作都会影响主调函数中的实参。
- 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量。引用传递通过间接寻址的方式操作到主调函数中的相关变量,而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数。如果想要指针参数传递来改变主调函数中的相关变量,需要使用指向指针的指针或指针引用。
- 从编译角度来说,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的内容是变量名及变量名所对应的地址。指针变量在符号表上对应的地址值为指针变量的地址,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成之后就不能修改,因此,指针可以改变其指向对象,而引用对象则不能修改。
3. static
- (隐藏)当同时编译多个文件时,未使用static的全局变量和全局函数具有全局可见性,而使用static关键字的只能在本文件中使用。
- (存储方式)使用static关键字的变量改变了其声明周期,将变量存放于静态数据区/全局区,当整个程序运行结束之后才会释放。
- (默认0)static变量默认初始化为0。
- (类)
1) 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此,其值在下次调用时仍然保持上一次的值
2) 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其他函数访问。
3) 在模块内的static函数只能被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内。
4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
5)在类中的static成员函数属于整个类所拥有,这个函数不接受this指针,因此只能访问类的static成员变量。
6)static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化。
7)由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针,this指针是指向本对象的指针,正因为没有this指针,所有不能访问非static的类成员,只能访问static修饰的类成员。
8)static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义。静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个 vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。
总结:1.static成员变量类内声明,类外初始化,全局只有一份。2. static成员函数是类,而不是对象,全局只有一份,没有this指针,不能调用非static成员,也不能使用virtual。3. 共同:static不改变作用范围,只有一份拷贝。
4. const
-
(基本数据类型)限制变量的更改。
-
(指针和引用)如果 const 位于⼩星星的左侧,则 const 就是⽤来修饰指针所指向的变量,即指
针指向为常量;如果 const 位于⼩星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。 -
(函数)修饰形参:函数内部不能改变其值;修饰返回值:保护返回值
-
(类)
-
(修饰成员变量)只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔是可以改变的。
因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进⾏。
-
(修饰成员函数)不能修改类的成员变量,类的常对象只能访问类的常成员函数。const 成员函数的主要⽬的是防⽌成员函数修改对象的内容。对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。const 关键字和 static 关键字对于成员函数来说是不能同时使⽤的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数⼜必须具体到某⼀个函数。
const 成员函数中如果实在想修改某个变量,可以使⽤ mutable 进⾏修饰。
注:const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
-
(修饰类对象)常量对象,常量对象只能调⽤常量函数,别的成员函数都不能调⽤。⾮常量对象可以调⽤类中的 const 成员函数,也可以调⽤⾮ const 成员函数。
原因:对象调⽤成员函数时,在形参列表的最前⾯加⼀个形参 this,但这是隐式的。this 指针是默认指向调⽤函数的当前对象的,所以,很⾃然,this 是⼀个常量指针 test * const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即⼩括号)后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test *const,即不可以通过 this 指针来改变指向对象的值。
-
-
补充:
const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;
5. 指针和const的用法
当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。
const int * p1; //常量指针,它指向的值不能修改
int * const p2; //指针常量 ,它不能在指向别的变量,但指向(变量)的值可以修改。
const int *const p3; //常量指针常量 。它既不能再指向别的常量,指向的值也不能修改。
6. mutable
- 如果需要在const成员方法中修改一个成员变量的值,那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制;
- 可以认为mutable的变量是类的辅助状态,但是只是起到类的一些方面表述的功能,修改他的内容我们可以认为对象的状态本身并没有改变的。实际上由于const_cast的存在,这个概念很多时候用处不是很到了。
7. extern
- extern声明外部变量/函数:在函数或者文件外部定义的变量/函数
- extern修饰符可用于指示C或者C++函数的调用规范
比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。
8. int转字符串,字符串转int?strcat,strcpy,strncpy,memset,memcpy的内部实现?
int->str: std::to_string
str->int: stoi, stol, stoll
//把 src 所指向的字符串复制到 dest,注意:dest定义的空间应该⽐src⼤。
char* strcpy(char *dest,const char *src) {
char *ret = dest;
assert(dest!=NULL);//优化点1:检查输⼊参数
assert(src!=NULL);
while(*src!='\0')
*(dest++)=*(src++);
*dest='\0';//优化点2:⼿动地将最后的'\0'补上
return ret;
}
//考虑内存᯿叠的字符串拷⻉函数 优化的点
char* strcpy(char *dest,char *src) {
char *ret = dest;
assert(dest!=NULL);
assert(src!=NULL);
memmove(dest,src,strlen(src)+1);
return ret;
}
//把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
char* strcat(char *dest,const char *src) {
//1. 将⽬的字符串的起始位置先保存,最后要返回它的头指针
//2. 先找到dest的结束位置,再把src拷⻉到dest中,记得在最后要加上'\0'
char *ret = dest;
assert(dest!=NULL);
assert(src!=NULL);
while(*dest!='\0')
dest++;
while(*src!='\0')
*(dest++)=*(src++);
*dest='\0';
return ret;
}
//把 str1 所指向的字符串和 str2 所指向的字符串进⾏⽐较。
//该函数返回值如下:
//如果返回值 < 0,则表示 str1 ⼩于 str2。
//如果返回值 > 0,则表示 str1 ⼤于 str2。
//如果返回值 = 0,则表示 str1 等于 str2。
int strcmp(const char *s1,const char *s2) {
assert(s1!=NULL);
assert(s2!=NULL);
while(*s1!='\0' && *s2!='\0') {
if(*s1>*s2)
return 1;
else if(*s1<*s2)
return -1;
else {
s1++,s2++;
}
}
//当有⼀个字符串已经⾛到结尾
if(*s1>*s2)
return 1;
else if(*s1<*s2)
return -1;
else
return 0;
}
//在字符串 str1 中查找第⼀次出现字符串 str2 的位置,不包含终⽌符 '\0'。
char* strstr(char *str1,char *str2) {
char* s = str1;
assert(str1!='\0');
assert(str2!='\0');
if(*str2=='\0')
return NULL;//若str2为空,则直接返回空
while(*s!='\0') {//若不为空,则进⾏查询
char* s1 = s;
char* s2 = str2;
while(*s1!='\0' && *s2!='\0' && *s1==*s2)
s1++,s2++;
if(*s2=='\0')
return s1;//若s2先结束
if(*s2!='\0' && *s1=='\0')
return NULL;//若s1先结束⽽s2还没结束,则返回空
s++;
}
return NULL;
}
//模拟实现memcpy函数 从存储区 str2 复制 n 个字符到存储区 dst。
void* memcpy(void* dest, void* src, size_t num) {
void* ret = dest ;
size_t i = 0 ;
assert(dest != NULL ) ;
assert(src != NULL) ;
for(i = 0; i < num; i++) {
//因为void* 不能直接解引⽤,所以需要强转成char*再解引⽤
//此处的void*实现了泛型编程
*(char*) dest = *(char*) src ;
dest = (char*) dest + 1 ;
src = (char*) src + 1 ;
}
return ret;
}
//考虑内存重叠的memcpy函数 优化的点
void* memmove(void* dest, void* src, size_t num) {
char* p1 = (char*)dest;
char* p2 = (char*)src;
if(p1<p2) {//p1低地址p2⾼地址
for(size_t i=0; i!=num; ++i)
*(p1++) = *(p2++);
}
else {
//从后往前赋值
p1+=num-1;
p2+=num-1;
for(size_t i=0; i!=num; ++i)
*(p1--) = *(p2--);
}
return dest;
}
9. 深拷贝与浅拷贝?
浅拷贝:仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
深拷贝:在计算机中开辟了一块新的内存地址用于存放复制的对象。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
10. C++模板是什么,底层怎么实现的?
模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。
模板是一种对类型进行参数化的工具;
通常有两种形式:函数模板和类模板;
函数模板针对仅参数类型不同的函数;
类模板针对仅数据成员和成员函数类型不同的类。
模板类和模板函数的区别是什么?
函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加,而函数模板不必
-
编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
-
这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。
11. C语言struct和C++struct区别
- C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。
- C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
- C++中,struct的成员默认访问说明符为public(为了与C兼容),class中的默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。
- struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名
总结:
struct | C语言 | C++ |
---|---|---|
成员 | 没有函数成员,只有数据 | 函数和数据都可以有 |
访问权限 | 没有访问权限的设定,及对外不隐藏数据 | 有访问权限的设定private,public,protected |
是否可以继承 | 不可以 | 有继承关系 |
12. 虚函数可以声明为inline吗?
- 虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
- 虚函数要求在运行时进行类型确定,而内敛函数要求在编译期完成相关的函数替换,所以不可以声明;
13. 类成员初始化方式?为什么用成员初始化列表会快一些?
- 类初始化方式:
初始化方式一:列表初始化
class A
{
public:
int a; // 列表初始化
A(int a_):a(a_){}
};
初始化方式二:构造函数初始化
class A
{
public:
int a; // 初始化列表
A(int a_, bool b) { a = a_; }
};
初始化方式三:声明时初始化(也称就地初始化,c++11后支持)
class A
{
public:
int a = 1; // 声明时初始化
A() {}
};
优先级:声明时初始化->初始化列表->构造函数初始化
对于构造函数初始化,是在所有的数据成员被分配内存空间后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
- 初始化列表方法快的原因?
方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。
14. 列表初始化的使用场景?
当初始化⼀个引⽤成员变量时;
初始化⼀个 const 成员变量时;
当调⽤⼀个基类的构造函数,⽽构造函数拥有⼀组参数时;
当调⽤⼀个成员类的构造函数,⽽他拥有⼀组参数;
成员初始化列表做了什么
① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;
c++规定const成员和引用成员必须在初始化列表中初始化,而不能在构造函数体内初始化?
为什么需要这么规定呢?因为在进入构造函数体内时,实际上变量都已经初始化完毕了,即引用变得和const变量都已经用不确定的值初始化好了,构造函数内能做的只有赋值,而const类型和引用类型是不可以赋值的。所以,需要在初始化列表中初始化。
15. 构造函数和析构函数的作用,如何起作用?
- 构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,就是你不写,编译器也自动调用一次。
- 析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。 当撤销对象时,编译器也会自动调用析构函数。 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。
16. 构造函数的执行顺序?析构函数的执行顺序?
构造函数的执行顺序如下:
① 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
② 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
③ 派生类构造函数。
析构函数的执行顺序如下:
① 调用派生类的析构函数;
② 调用成员类对象的析构函数;
③ 调用基类的析构函数。
17. 构造函数为什么不能为虚函数?析构函数为什么要虚函数?
- 构造函数为什么不能为虚函数?
-
从存储空间角度
虚函数对应一个虚表,可是这个虚表其实是存储在对象的内存空间的。
那么问题来了,如果构造函数是虚函数,就要通过虚表来调用,可是对象空间还没有实例化,也就是内存空间还没有,无法找到虚表,所以构造函数不能是虚函数。 -
从使用角度
虚函数主要用于在信息不全的情况下,能够使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。
另外,虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数,从而实现多态,也就是实现“一个接口,多种方法”。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此规定构造函数不能是虚函数。
- 析构函数为什么必须是虚函数?
因为析构函数的调用顺序是先调用子类的析构函数,再向外调用父类的析构函数。
如果父类的析构函数不是虚函数的话,就会直接调用父类的析构函数,而不调用子类的析构函数,就会造成子类的有些内存来不及等释放,造成内存泄露。
18. 构造函数和析构函数可以调用虚函数吗?
- 在构造函数和析构函数中最好不要调用虚函数;
- 构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数;
- 即使构造函数或者析构函数如果能成功调用虚函数, 程序的运行结果也是不可控的。
(1)不要在构造函数中调用虚函数的原因:因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化, 因此调用子类的虚函数是不安全的,故而C++不会进行动态联编。
(2)不要在析构函数中调用虚函数的原因:析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。
19. 构造函数和析构函数可否抛出异常?
构造函数:
-
构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。
2. 因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。- 构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。(如何保证???使用auto_ptr)
析构函数
-
不要在析构函数中抛出异常!虽然C++并不禁止析构函数抛出异常,但这样会导致程序过早结束或出现不明确的行为。
-
如果某个操作可能会抛出异常,class应提供一个普通函数(而非析构函数),来执行该操作。目的是给客户一个处理错误的机会。
-
如果析构函数中异常非抛不可,那就用try catch来将异常吞下,但这样方法并不好,我们提倡有错早些报出来。
20. 类如何实现只能静态分配和只能动态分配?
- 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建
- 建立类的对象有两种方式:
① 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
② 动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;
③ 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有。
21. 如果想将某个类用作基类,为什么该类必须定义而非声明?
派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。
22. 什么情况会自动生成默认构造函数?
- 含有类对象数据成员,该类对象类型有默认构造函数
- 基类带有默认构造函数的派生类
- 带有虚函数的类
- 带有一个虚基类的类
23. 什么是类的继承?
概念:
所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;
特点:
子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;
24. 什么是组合?
- 一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。
- 创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体,析构函数调用顺序相反。
25. 抽象基类为什么不能创建对象?
抽象类尚未实现方法,所以不能创建对象。
创建对象去调用方法是指做明确的事情,而这个抽象方法并不明确,所以只有继承抽象类去实现抽象方法才可以。
26. 类什么时候会析构?
- 对象生命周期结束,被销毁时;
- delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
- 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
27. 为什么友元函数必须在类内部声明?
因为编译器必须能够读取这个结构的声明以理解这个数据类型的所有规则
28. 面向对象的三大特性,并举例说明
C++ 面向对象的三⼤特征是:封装、继承、多态。
所谓封装
就是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让信任的类或者对象操作,对不可信的进⾏信息隐藏。⼀个类就是⼀个封装了数据以及操作这些数据的代码的逻辑实体。在⼀个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种⽅式,对象对内部数据提供了不同级别的保护,以防⽌程序中⽆关的部分意外的改变或错误的使⽤了对象的私有部分。
所谓继承
是指可以让某个类型的对象获得另⼀个类型的对象的属性的⽅法。它⽀持按级分类的概念。继承是指这样⼀种能⼒:它可以使⽤现有类的所有功能,并在⽆需᯿新编写原来的类的情况下对这些功能进⾏扩展。通过继承创建的新类称为“⼦类”或者“派⽣类”,被继承的类称为“基类”、“⽗类”或“超类”。继承的过程,就是从⼀般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。
继承概念的实现⽅式有两类:
实现继承:实现继承是指直接使⽤基类的属性和⽅法⽽⽆需额外编码的能⼒。
接⼝继承:接⼝继承是指仅使⽤属性和⽅法的名称、但是⼦类必需提供实现的能⼒。
所谓多态
就是向不同的对象发送同⼀个消息,不同对象在接收时会产⽣不同的⾏为(即⽅法)。即⼀个接⼝,可以实现多种⽅法。
多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调⽤,在编译器编译期间就可以确定函数的调⽤地址,并产⽣代码,则是静态的,即地址早绑定。⽽如果函数调⽤的地址不能在编译器期间确定,需要在运⾏时才确定,这就属于晚绑定。
29. 多态的实现
多态其实⼀般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数⽣成符号表时的不同规则,重载只是⼀种语⾔特性,与多态⽆关,与⾯向对象也⽆关,但这⼜是 C++中增加的新规则,所以也算属于 C++,所以如果非要说重载算是多态的⼀种,那就可以说:多态可以分为静态多态和动态多态。
静态多态其实就是重载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表来决定;
动态多态是指通过⼦类重写⽗类的虚函数来实现的,因为是在运⾏期间决定调⽤的函数,所以称为动态多态,⼀般情况下我们不区分这两个时所说的多态就是指动态多态。
动态多态的实现与虚函数表,虚函数指针相关。
扩展:子类是否要重写⽗类的虚函数?子类继承父类时, ⽗类的纯虚函数必须重写,否则⼦类也是⼀个虚类不可实例化。 定义纯虚函数是为了实现⼀个接口,起到⼀个规范的作⽤,规范继承这个类的程序员必须实现这个函数。
30. 继承机制中对象之间如何转换?指针和引用之间如何转换?
- 向上类型转换
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。 - 向下类型转换
将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。
31. 组合与继承优缺点?
继承
继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点:
①:父类的内部细节对子类是可见的。
②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点:
①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。
②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
组合的缺点:
①:容易产生过多的对象。
②:为了能组合多个对象,必须仔细对接口进行定义。
32. 左值右值
- 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
- C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
- 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
- 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。
33. C语言的编译链接过程?
源代码-->预处理-->编译-->优化-->汇编-->链接–>可执行文件
- 预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件,.ii预处理后的C++文件。 - 编译阶段
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件 - 汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件 - 链接阶段
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
34. 函数指针?
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
用途:调⽤函数和做函数的参数,⽐如回调函数。
指向函数的指针变量没有 ++ 和 – 运算。
详细学习链接
35. 说说你对c和c++的看法,c和c++的区别?
-
C是面向过程的语言,C++是面向对象的语言。两者的区别
-
C++ 有新增的语法和关键字,语法的区别有头⽂件的不同和命名空间的不同。 C和C++动态管理内存的方法不一样,C是使用malloc/free函数,而C++除此之外还有new/delete关键字,C++中有引用。
-
C中的struct和C++的类,C++的类是C所没有的,但是C中的struct是可以在C++中正常使用的,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用,而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private
-
C++三大特性,C++支持函数重载,虚函数等,而C不支持函数重载,
-
C++全部变量的默认链接属性是外链接,而C是内连接
-
C 中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以(如果不进行&,解引用的操作的话,是存放在符号表的,不开辟内存)
-
C++ 中增加了模板还重用代码,提供了更加强⼤的 STL 标准库
36. c/c++的内存分配,详细说一下栈、堆、静态存储区?
代码区:存放程序的二进制代码
全局区/静态存储区:存储全局变量和静态变量
常量区:存储常量
栈区:由编译器分配和释放内存,存放局部变量和函数参数
堆区:由程序员进行管理,需要手动进行malloc/free new/delete进行分配和回收。如果程序员忘记回收,会在程序执行完成后由操作系统自动回收
37. 堆与栈的区别?
- 管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
- 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的。
- 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
- 生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
- 分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
- 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
38. ++i 和 i++ 的区别
++i (前置加加)先⾃增 1再返回,i++ (后置加加)先返回 i 再⾃增1。
前置加加不会产⽣临时对象,后置加加必须产⽣临时对象,临时对象会导致效率降低
++i 实现
int& int::operator++() {
*this += 1;
return *this;
}
i++实现
const int int::operator(int) {
int oldvalue = *this;
++(*this);
return oldvalue;
}
39. 分别写出 bool,int,float,指针类型的变ᰁ a 与“零”的⽐较语句。
bool: if (!a) or if (a)
int: if (a == 0)
float: const EXPRESSION EXP = 0.0000001 if ( a <= EXP && a >= -EXP)
pointer: if (a != NULL) or if (a == NULL)
⽆论是 float 还是 double 类型的变量,都有精度限制。所以⼀定要避免将浮点变量⽤“==”或“!=”与数字⽐较,应该设法转化成“>=”或“<=”形式
40. 函数传递参数的几种方式
值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
指针传递:也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。
引⽤传递:实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯
41. 野指针是什么?如何检测内存泄漏?
-
野指针:指向内存被释放的内存或者没有访问权限的内存的指针。
-
野指针”的成因主要有3种:
① 指针变量没有被初始化
② 指针p被free或者delete之后,没有置为NULL
③ 指针操作超越了变量的作用范围 -
如何避免野指针:
① 对指针进行初始化
② 指针用完后释放内存,将指针赋NULL。
42. 悬空指针和野指针有什么区别?
野指针(wild pointer):就是没有被初始化过的指针。
悬空指针:是指针最初指向的内存已经被释放了的⼀种指针。
43. 内存泄漏
-
什么是内存泄漏?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。 -
内存泄漏的分类
(1) 堆内存泄漏﹐(Heap leak)
对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
(2) 系统资源泄露(Resource Leak)
主要指程序使用系统分配的资源比如Bitmap,handle ,SOCKET等没有使用相应的函数释放掉导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
(3) 没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。 -
什么操作会导致内存泄露?
指针指向改变,未释放动态分配内存。 -
如何防止内存泄露?
将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使用智能指针
44. new和malloc的区别?
1、 new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;
2、 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
3、 new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4、 new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
5、 new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
6、 new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会
45. delete p;与delete[]p,allocator
1、 动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;
2、 new动态数组返回的并不是数组类型,而是一个元素类型的指针;
3、 delete[]时,数组中的元素按逆序的顺序进行销毁;
4、 new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。
46. new和delete的实现原理, delete是如何知道释放内存的大小的额?
1、 new简单类型直接调用operator new分配内存;而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;对于简单类型,new[]计算好大小后调用operator new;对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
① new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
② 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
③ 对象被分配了空间并构造完成,返回一个指向该对象的指针。
2、 delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
3、 需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
47. 为什么内存对齐
1、 平台原因(移植原因)
- 不是所有的硬件平台都能访问任意地址上的任意数据的;
- 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2、性能原因:
- 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
- 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
48. define、const、typedef、inline使用方法?
一、 const与#define的区别:
- const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
- define只在预处理阶段起作用,简单的文本替换,而const在编译、链接过程中起作用;
- define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
- define预处理后,占用代码段空间,const占用数据段空间;
- const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
- define独特功能,比如可以用来防止文件重复引用。
二、 #define和别名typedef的区别
- 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;
- 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
- 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。
三、 define与inline的区别
- #define是关键字,inline是函数;
- 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
- inline函数有类型检查,相比宏定义比较安全;
49. #include 的顺序以及尖叫括号和双引号的区别
<>尖括号表示编译器只在系统默认目录或尖括号内的工作目录下搜索头文件,并不去用户的工作目录下寻找,所以一般尖括号用于包含标准库文件;
""双引号表示编译器先在用户的工作目录下搜索头文件,如果搜索不到则到系统默认目录下去寻找,所以双引号一般用于包含用户自己编写的头文件。
50. C++中类成员的访问权限和继承权限问题
- 三种访问权限
① public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被 访问,在类外也是可以被访问的,是类对外提供的可访问接口;
② private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
③ protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。 - 三种继承方式
① 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
② 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;
③ 若继承方式是protected,基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。
51. cout和printf有什么区别?
cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。
cout是有缓冲输出:
cout < < "abc " < <endl;
或cout < < "abc\n ";cout < <flush; 这两个才是一样的.
endl相当于输出回车后,再强迫缓冲输出。
flush立即强迫缓冲输出。
printf是无缓冲输出。有输出时立即输出
52. 定义和声明的区别
- 如果是指变量的声明和定义
从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。 - 如果是指函数的声明和定义
声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。
定义:一般在源文件里,具体就是函数的实现过程 写明函数体。
53. C++类型转换有四种
- static_cast能进行基础类型之间的转换,也是最常看到的类型转换。它主要有如下几种用法:
1 . 用于类层次结构中父类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成父类表示)是安全的;
2 . 进行下行转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的;
3 . 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
4 . 把void指针转换成目标类型的指针(不安全!!)
5 . 把任何类型的表达式转换成void类型。 - const_cast运算符用来修改类型的const或volatile属性。除了去掉const 或volatile修饰之外, type_id和expression得到的类型是一样的。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
- reinterpret_cast它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
- dynamic_cast 主要用在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。转型失败会返回null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。 dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此 dynamic_cast 存在一定的效率损失。当使用dynamic_cast时,该类型必须含有虚函数,这是因为dynamic_cast使用了存储在VTABLE中的信息来判断实际的类型,RTTI运行时类型识别用于判断类型。typeid表达式的形式是typeid(e),typeid操作的结果是一个常量对象的引用,该对象的类型是type_info或type_info的派生。
54. 全局变量和static变量的区别
1、全局变量(外部变量)的说明之前再冠以static就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。
这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。
2.static函数与普通函数有什么区别?
static函数与普通的函数作用域不同。尽在本文件中。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。
static函数与普通函数最主要区别是static函数在内存中只有一份,普通静态函数在每个被调用中维持一份拷贝程序的局部变量存在于(堆栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)
55. 静态成员与普通成员的区别
- 生命周期
静态成员变量从类被加载开始到类被卸载,一直存在;
普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束; - 共享方式
静态成员变量是全类共享;普通成员变量是每个对象单独享用的; - 定义位置
普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区; - 初始化位置
普通成员变量在类中初始化;静态成员变量在类外初始化; - 默认实参
可以使用静态成员变量作为默认实参,
56. ifdef endif
- 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。
- 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。
57. 隐式转换,如何消除隐式转换?
- C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。
- 如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。
58. 虚函数的内存结构,那菱形继承的虚函数内存结构呢
菱形继承的定义是:两个子类继承同一父类,而又有子类同时继承这两个子类。例如a,b两个类同时继承c,但是又有一个d类同时继承a,b类。
59. 多继承的优缺点,作为一个开发者怎么看待多继承
- C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。
- 多重继承的优点很明显,就是对象可以调用多个基类中的接口;
- 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性
- 加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。
- 使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。
60. 智能指针的作用
- C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
- 智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
- 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象
- unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
- 智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
- weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
1、auto_ptr(C++98 的⽅案,C11 已抛弃)采⽤所有权模式
auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错
此时不会报错,p2 剥夺了 p1 的所有权,但是当程序运⾏时访问 p1 将会报错。所以 auto_ptr 的缺点是:存在潜在的内存崩溃问题
2、unique_ptr(替换 auto_ptr )
unique_ptr 实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对象。它对于避免资源泄露特别有⽤。
unique_ptr<string> p3 (new string (auto));//#4
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错
编译器认为 p4=p3 ⾮法,避免了 p3 不再指向有效数据的问题。
因此,unique_ptr ⽐ auto_ptr 更安全。
3、shared_ptr(共享型,强引⽤)
shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在“最后⼀个引⽤被销毁”时候释放。从名字 share 就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被几个指针共享。
可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr,unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。
4、weak_ptr(弱引⽤)
weak_ptr 是⼀种不控制对象⽣命周期的智能指针,它指向⼀个 shared_ptr 管理的对象。进⾏该对象的内存管理的是那个强引⽤的 shared_ptr。
weak_ptr 只是提供了对管理对象的⼀个访问⼿段。weak_ptr 设计的⽬的是为配合 shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作,它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象构造,,它的构造和析构不会引起引⽤记数的增加或减少。
weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr。
当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把其中⼀个改为weak_ptr就可以。
61. auto_ptr作用
- auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;
- auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;
- auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;
- 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;
- Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;
- auto_ptr支持所拥有的指针类型之间的隐式类型转换。
- 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;
- T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。
62. 动态联编与静态联编
- 在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;
- 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。
- 动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)
- 实现动态联编三个条件:
必须把动态联编的行为定义为类的虚函数;
类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来;
必须先使用基类指针指向子类型的对象,然后直接或间接使用基类指针调用虚函数;
静态绑定和动态绑定的介绍
- 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
- 对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
- 静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
- 动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
63. strcpy和memcpy的区别
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
64. volatile关键字的作用?
volatile 三个特性
易变性:在汇编层⾯反映出来,就是两条语句,下⼀条语句不会直接使⽤上⼀条语句对应的 volatile 变量的寄存器内容,⽽是从新从内存中读取。
不可优化性:volatile 告诉编译器,不要对我这个变ᰁ进⾏各种激进的优化,甚⾄将变量直接消除,保证程序员写在代码中的指令,⼀定会被执⾏。
顺序性:能够保证 volatile 变量之间的顺序性,编译器不会进⾏乱序优化
65. 空类会默认添加哪些东西?怎么写?
- Empty(); // 缺省构造函数//
- Empty(const Empty& ); // 拷贝构造函数//
- ~Empty(); // 析构函数//
- Empty& operator=( const Empty& ); // 赋值运算符//
66. 标准库是什么?
- C++ 标准库可以分为两部分:
标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言。
面向对象类库: 这个库是类及其相关函数的集合。 - 输入/输出 I/O、字符串和字符处理、数学、时间、日期和本地化、动态分配、其他、宽字符函数
- 标准的 C++ I/O 类、String 类、数值类、STL 容器类、STL 算法、STL 函数对象、STL 迭代器、STL 分配器、本地化库、异常处理类、杂项支持库
67. 为什么拷贝构造函数必须传引用不能传值?
为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。
68. 什么情况下会调⽤拷⻉构造函数(三种情况)
类的对象需要拷⻉时,拷⻉构造函数将会被调⽤,以下的情况都会调⽤拷⻉构造函数:
⼀个对象以值传递的⽅式传⼊函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间中。
⼀个对象以值传递的⽅式从函数返回,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回值。
⼀个对象需要通过另外⼀个对象进⾏初始化。
69. 空类的大小是多少?为什么?
- C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
- C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
- 带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
- C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
70. 什么情况用指针当参数,什么时候用引用,为什么?
- 使用引用参数的主要原因有两个:
程序员能修改调用函数中的数据对象
通过传递引用而不是整个数据–对象,可以提高程序的运行速度 - 一般的原则:
对于使用引用的值而不做修改的函数:
如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;
如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;
如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;
如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递); - 对于修改函数中数据的函数:
如果数据是内置数据类型,则使用指针
如果数据对象是数组,则只能使用指针
如果数据对象是结构,则使用引用或者指针
如果数据是类对象,则使用引用
71. 大内存申请时候选用哪种?C++变量存在哪?变量的大小存在哪?符号表存在哪?
- 大内存申请时,采用堆申请空间,用new申请;
- 不同的变量存储在不同的地方,局部变量、全局变量、静态变量;
- C++对变量名不作存储,在汇编以后不会出现变量名,变量名作用只是用于方便编译成汇编代码,是给编译器看的,是方便人阅读的
72. 静态函数能定义为虚函数吗?常函数?
1、static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。2. 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.虚函数的调用关系:this -> vptr -> vtable ->virtual function
73. 类对象的大小
- 类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
- 内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
- 虚函数的话,会在类对象插入vptr指针,加上指针大小;
- 当该该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在在派生类中的空间中,也会对派生类进行扩展。
74. 构造函数的执行算法?
- 在派生类构造函数中,所有的虚基类及上一层基类的构造函数调用;
- 对象的vptr被初始化;
- 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做;
- 执行程序员所提供的代码;
75. 构造函数的扩展过程?
- 记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并与成员的声明顺序为顺序;
- 如果一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么默认构造函数必须被调用;
- 如果class有虚表,那么它必须被设定初值;
- 所有上一层的基类构造函数必须被调用;
- 所有虚基类的构造函数必须被调用。
程序员定义的析构函数被扩展的过程?
- 析构函数函数体被执行;
- 如果class拥有成员类对象,而后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调用;
- 如果对象有一个vptr,现在被重新定义
- 如果有任何直接的上一层非虚基类拥有析构函数,则它们会以声明顺序被调用;
- 如果任何虚基类拥有析构函数
76. 哪些函数不能是虚函数
- 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
- 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
- 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
- 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
- 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
77. sizeof 和strlen 的区别
- strlen计算字符串的具体长度(只能是字符串),不包括字符串结束符。返回的是字符个数。
- sizeof计算声明后所占的内存数(字节大小),不是实际长度。
- sizeof是一个取字节运算符,而strlen是个函数。
- sizeof的返回值=字符个数*字符所占的字节数,字符实际长度小于定义的长度,此时字符个数就等于定义的长度。若未给出定义的大小,分类讨论,对于字符串数组,字符大 小等于实际的字符个数+1;对于整型数组,字符个数为实际的字符个数。字符串每个字符占1个字节,整型数据每个字符占的字节数需根据系统的位数类确定,32位占4个字节。
- sizeof可以用类型做参数,strlen只能用char*做参数,且必须以‘\0’结尾,sizeof还可以用函数做参数;
- 数组做sizeof的参数不退化,传递给strlen就退化为指针;
78. 简述strcpy、sprintf与memcpy的区别
- 操作对象不同
① strcpy的两个操作对象均为字符串
② sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串
③ memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。 - 执行效率不同
memcpy最高,strcpy次之,sprintf的效率最低。 - 实现功能不同
① strcpy主要实现字符串变量间的拷贝
② sprintf主要实现其他数据类型格式到字符串的转化
③ memcpy主要是内存块间的拷贝。
79. 局部变量全局变量的问题?
- 局部会屏蔽全局。要用全局变量,需要使用"::"局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。
- 如何引用一个已经定义过的全局变量,可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。
- 全局变量可不可以定义在可被多个.C文件包含的头文件中,在不同的C文件中以static形式来声明同名全局变量。可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错
80. 数组和指针的区别?
- 数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);
- 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof§,p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。
- 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。
- 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;
- 在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的。
81. C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private?
- 将类定义为抽象基类或者将构造函数声明为private;
- 不允许类外部创建类对象,只能在类内部创建对象
82. 如何禁止自动生成拷贝构造函数?
- 为了阻止编译器默认生成拷贝构造函数和拷贝赋值函数,我们需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。
- 类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误;
- 针对上述两种情况,我们可以定一个base类,在base类中将拷贝构造函数和拷贝赋值函数设置成private,那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关的操作。
83. 虚函数与纯虚函数的区别在于
- 纯虚函数只有定义没有实现,虚函数既有定义又有实现;
- 含有纯虚函数的类不能定义对象,含有虚函数的类能定义对象;
84. 为什么要用static_cast转换而不用c语言中的转换?
- 更加安全;
- 更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图
85. 成员函数里memset(this,0,sizeof(*this))会发生什么
- 有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;
- 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;
- 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。
86. 回调函数的作用
- 当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;
- 回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;
- 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;
- 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
87. C++11新特性
C++11 的特性主要包括下⾯几个方面:
提⾼运⾏效率的语⾔特性:右值引用、泛化常量表达式
原有语法的使⽤性增强:初始化列表、统⼀的初始化语法、类型推导、范围 for 循环、Lambda 表达式、final和 override、构造函数委托
语⾔能⼒的提升:空指针 nullptr、default 和 delete、⻓整数、静态 assert
C++ 标准库的更新:智能指针、正则表达式、哈希表等
1、空指针 nullptr
2、Lambda 表达式
Lambda 表达式实际上就是提供了⼀个类似匿名函数的特性,⽽匿名函数则是在需要⼀个函数,但是⼜不想费⼒去命名⼀个函数的情况下去使⽤的。
利⽤ lambda 表达式可以编写内嵌的匿名函数,⽤以替换独⽴函数或者函数对象,并且使代码更可读。
3、右值引⽤
4、泛化的常量表达式
constexpr int N = 5; // N 变成了⼀个只读的值
int arr[N]; // OK
constexpr 告诉编译器这是⼀个编译期常量,甚⾄可以把⼀个函数声明为编译期常量表达式。
5、初始化列表
6、统⼀的初始化语法
字符串,列表。。。均可使⽤“{}-初始化变量列表”
7、类型推导
C++ 提供了 auto 和 decltype 来静态推导类型,
8、基于范围的for循环
for (int n : nums);
9、构造函数委托
这使得构造函数可以在同⼀个类中⼀个构造函数调⽤另⼀个构造函数,从⽽达到简化代码的目的
10、final 和 override
C++11 提供了 final 来禁⽌虚函数被重写/禁⽌类被继承, override 来显示地重写虚函数。
11、default 和 delete
12、静态 assertion
13、智能指针
14、正则表达式
15、增强的元组
在 C++ 中本已有⼀个 pair 模板可以定义⼆元组,C++11 更进⼀步地提供了边⻓参数的 tuple 模板
16、哈希表
88. 静态库与动态库
89. C++resize和reverse修改容器大小
resize用于改变容器的大小,即改变容器中元素的个数。当你需要增加或减少容器的元素数量时,resize是一个有用的工具。如果新的大小比当前大小大,容器将会增加元素,新元素的值将会是默认值;如果新的大小比当前大小小,多余的元素将会被删除。
reserve用于预分配容器的存储空间,但并不改变容器中的元素数量。这对于避免因为频繁添加元素而导致的重新分配和拷贝操作非常有帮助,从而提高程序的性能。
90. push_back 和 emplace_back
当调用push_back() 或insert() 成员函数时,是把元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个 emplace 系列函数时,则是将相应参数传递给元素类型的构造函数。这样emplace_back() 能就地通过参数构造对象,不需要拷贝操作,相比push_back() 能更好的避免内存的拷贝和移动,提升容器插入元素的性能。大多数情况都应该使用 emplace 系列函数:emplace; emplace_back; emplace_hit; emplace_fornt; emplace_after.
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
1. Linux常用命令
- ls命令,不仅可以查看linux文件包含的文件,而且可以查看文件权限;
- cd命令,切换当前目录到dirName
- pwd命令,查看当前工作目录路径;
- mkdir命令,创建文件夹
- rm命令,删除一个目录中的一个或多个文件或目录
- rmdir命令,从一个目录中删除一个或多个子目录项,
- mv命令,移动文件或修改文件名
- cp命令,将源文件复制至目标文件,或将多个源文件复制至目标目录
- cat命令,显示文件内容;
- touch命令,创建一个文件
- vim命令,
- which命令查看可执行文件的位置,whereis查看文件的位置,find实际搜寻硬盘查询文件名称;
- chmod命令,用于改变linux系统文件或目录的访问权限,421,ewr
- tar命令,用来压缩和解压文件。tar本身不具有压缩功能,只具有打包功能,有关压缩及解压是调用其它的功能来完成。
- chown命令,将指定文件的拥有者改为指定的用户或组,用户可以是用户名或者用户ID;
- ln命令;
- grep命令,强大的文本搜索命令,grep全局正则表达式搜素;
- ps命令,用来查看当前运行的进程状态,一次性查看,如果需要动态连续结果使用top;
- top命令,显示当前系统正在执行的进程的相关信息,包括进程ID、内存占用率、CPU占用率等;
- kill命令,发送指定的信号到相应进程。不指定型号将发送SIGTERM(15)终止指定进程。
2. 文件处理grep,awk,sed这三个命令必知必会
- grep
grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。常用来在结果中搜索特定的内容。 - awk
awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件(或其他方式的输入流, 如重定向输入)逐行的读入(看作一个记录集), 把每一行看作一条记录,以空格(或\t,或用户自己指定的分隔符)为默认分隔符将每行切片(类似字段),切开的部分再进行各种分析处理。 - sed
sed更侧重对搜索文本的处理,如修改、删除、替换等等。sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。
3. 查询进程占用CPU的命令
- top
top命令可以实时动态地查看系统的整体运行情况,是一个综合了多方信息监测系统性能和运行信息的实用工具。 - ps
ps命令就是最基本进程查看命令。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等.总之大部分信息都是可以通过执行该命令得到。ps是显示瞬间进程的状态,并不动态连续;如果想对进程进行实时监控应该用top命令。
说⼀下平衡⼆叉树、⾼度平衡⼆叉树(AVL
⼆叉树:任何节点最多只允许有两个⼦节点,称为左⼦节点和右⼦节点,以递归的⽅式定义⼆叉树为,⼀个⼆叉树如果不为空,便是由⼀个根节点和左右两个⼦树构成,左右⼦树都可能为空。
⼆叉搜索树:⼆叉搜索树可以提供对数时间的元素插⼊和访问。节点的放置规则是:任何节点的键值⼀定⼤于其左⼦树的每⼀个节点的键值,并⼩于其右⼦树中的每⼀个节点的键值。因此⼀直向左⾛可以取得最⼩值,⼀直向右⾛可以得到最⼤值。插⼊:从根节点开始,遇键值较⼤则向左,遇键值较⼩则向右,直到尾端,即插⼊点。删除:如果删除点只有⼀个⼦节点,则直接将其⼦节点连⾄⽗节点。如果删除点有两个⼦节点,以右⼦树中的最⼩值代替要删除的位置。
平衡⼆叉树:其实对于树的平衡与否没有⼀个绝对的标准,“平衡”的⼤致意 思是:没有任何⼀个节点过深,不同的平衡条件会造就出不同的效率表现。以及不同的实现复杂度。有数种特殊结构例如 AVL-tree, RB-tree, AA-tree,均可以实现平衡⼆叉树。
AVL-tree :⾼度平衡的平衡⼆叉树(严格的平衡⼆叉树)AVL-tree 是要求任何节点的左右⼦树⾼度相差最多为 1的平衡⼆叉树。 当插⼊新的节点破坏平衡性的时候,从下往上找到第⼀个不平衡点,需要进⾏单旋转,或者双旋转进⾏调整。
说⼀下红⿊树(RB-tree)
红⿊树的定义:
性质1:每个节点要么是⿊⾊,要么是红⾊。
性质2:根节点是⿊⾊。
性质3:每个叶⼦节点(NIL)是⿊⾊。
性质4:每个红⾊结点的两个⼦结点⼀定都是⿊⾊。
性质5:任意⼀结点到每个叶⼦结点的路径都包含数量相同的⿊结点。