C++
常见考点:
- 三大特性
- static,const关键字
- C++和java以及python有什么不同,不同在哪里?
- C++类型转化是否安全?
- 堆和栈哪个更快
- 虚函数知道吗?析构函数为什么要用虚函数?
- 内存的划分,请堆区内存时,所需空间不足抛出异常后怎么办,怎样才能成功申请到内存
三大特性
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。使得代码模块化,隐藏细节。
多态:一个类实例的相同方法在不同情形有不同表现形式。
继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
重写:当一个子类继承一父类,而子类中的方法与父类中的方法的名称,参数个数、类型都完全一致时,就称子类中的这个方法重写了父类中的方法。
重载:一个类中的方法与另一个方法同名,但是参数表不同,这种方法称之为重载方法。
Java和C++的区别:
- Java是解释型语言,所谓的解释型语言,就是源码会先经过一次编译,成为中间码,中间码再被解释器解释成机器码。对于Java而言,中间码就是字节码(.class),而解释器在JVM中内置了。
- C++是编译型语言,所谓编译型语言,就是源码一次编译,直接在编译的过程中链接了,形成了机器码。
- C++比Java执行速度快,但是Java可以利用JVM跨平台。
- Java是纯面向对象的语言,所有代码(包括函数、变量)都必须在类中定义。而C++中还有面向过程的东西,比如是全局变量和全局函数。
- C++中有指针,Java中没有,但是有引用。
- C++支持多继承,Java中类都是单继承的。但是继承都有传递性,同时Java中的接口是多继承,类对接口的实现也是多实现。
- C++中,开发需要自己去管理内存,但是Java中JVM有自己的GC机制,虽然有自己的GC机制,但是也会出现OOM和内存泄漏的问题。C++中有析构函数,Java中Object的finalize方法。
- C++运算符可以重载,但是Java中不可以。同时C++中支持强制自动转型,Java中不行,会出现ClassCastException(类型不匹配)。
static和const
C++为了管理静态成员,提供了静态函数,静态函数对外提供接口。并且静态函数只能访问静态成员(原因:非静态成员函数在调用时 this指针被当作参数传进。而静态成员函数是属于类,不属于对象,没有 this 指针)。
static特点
1、 共享 : static 成员变量实现了同族类对象间信息共享;
2、 初始化:static 成员使用时必须初始化,且只能类外初始化。声明与实现分离时;只能初始化在实现部分(cpp 部分);
3 、类大小: static 成员类外存储,求类大小,并不包含在内;
4、 存储 : static 成员是命名空间属于类的全局变量,存储在 data 区 rw 段;
5、 访问 :可以通过类名访问(无对象生成时亦可),也可以通过对象访问。
static静态成员可以被类内的普通函数静态函数都可访问。
const 修饰数据成员,称为常数据成员。常数据成员可被普通成员函数和常成员函数来使用,不可以更改。const 常数据成员在使用前必须被初始化。也就是定义的同时就要初始化,之后不能再去赋值,只能使用。
1、const 修饰函数放在声明之后,实现体之前。
2、const 修饰函数不能修改类内的数据成员变量。
3、const 修饰函数只能调用 const 函数。非 const 函数可以调用 const 函数。
4、const 修饰的全局函数在定义和声明处都需要 const 修饰符。
const char ptr, char const ptr和char* const ptr
前两者相同,定义一个指向字符常量的指针,
第三种,定义一个指向字符的指针常数,不能修改ptr指针,但是可以修改该指针指向的内容。
const char *ptr==char const *ptr; 可以直接改变指针指向,但不能直接改变指针指向的值;*ptr=*ss;
char *const ptr; 可以直接改变指针指向的值,但不能直接改变指针指向;ptr[0]='s';
但两者都可以通过改变所指向的指针的内容,来改变它的值。
include头文件时,""和<>的区别
“”先在当前目录下查找头文件,若无便去第三方库文件里查找,最后去库文件查找,<>直接到库文件里查找
源文件到可执行文件的过程
预处理,生成.ii文件;编译,生成汇编文件.s文件;汇编,生成目标文件.o或.obj文件;链接,产生可执行文件.out或.exe文件
其中预处理主要:对#define宏展开,处理条件编译指令,处理#include指令。。。
C++与C的差别
C只能在函数开始处声明变量,C没有bool类型;C++有引用变量,c++强制类型转换多了int(num)这种
C和C++的函数在内存中存储的形式不一样,C++是存储函数名加参数,因此可以重载
指针和引用
指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
指针可以有多级,但是引用只能是一级。指针的值可以为空,也可能指向一个不确定的内存空间,但是引用的值不能为空,并且引用在定义的时候必须初始化为特定对象。
指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变引用对象了。sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小。
Lambda表达式
[]函数对象参数,()操作符重载函数参数,->返回值类型,{}函数体
函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)
[] (int x, int y) { return x + y; } // 隐式返回类型
[] (int& x) { ++x; } // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x; } // 没有参数,仅访问某个全局变量
[] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)
[] (int x, int y) -> int { int z = x + y; return z; }
[],[x, &y],[&],[=],[&, x],[=, &z]
auto my_lambda_func = [&](int x) { /* ... */ };
auto my_onheap_lambda_func = new auto([=](int x) { /* ... */ });
对 this 的捕获比较特殊,它只能按值捕获
而一个没有指定任何捕获的 lambda 函数,可以显式转换成一个具有相同声明形式函数指针
auto a_lambda_func = [](int x) { /* ... */ };
void (*func_ptr)(int) = a_lambda_func;
func_ptr(4); // calls the lambda
函数指针
函数的地址就是函数名,要将函数作为参数进行传递,必须传递函数名。
函数指针指向的是函数而非对象,和其他指针一样,函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
声明指针时,必须指定指针指向的数据类型,同样,声明指向函数的指针时,必须指定指针指向的函数类型,这意味着声明应当指定函数的返回类型以及函数的参数列表。
double cal(int); // prototype
double (*pf)(int); // 指针pf指向的函数, 输入参数为int,返回值为double
pf = cal; // 指针赋值
void estimate(int lines, double (*pf)(int)); // 函数指针作为参数传递
可以为函数指针赋一个 nullptr或者0 的整型常量表达式,表示该指针没有指向任何一个函数
构造函数
以下情况编译会生成默认构造函数
1.当该类的类对象数据成员有默认构造函数时。
2.当该类的基类有默认构造函数时。
3.当该类的基类为虚基类时。
4.当该类有虚函数时。
需要使用拷贝构造函数的情况:
- 对一个 object 做明确的初始化操作:A a; A a1=a;
- 当 object 被当做参数交给某个函数时 X xx; foo(xx);
- 函数传回一个 class object 时,X xx;return xx;
深拷贝,浅拷贝
成员初始化列表:当初始化一个reference member时;当初始化一个const member时;当调用一个base class的constructor,而它拥有一组参数时;当类成员对象没有无参构造函数;必须使用(生成顺序是由类中的成员声明顺序决定的,而不是初始化列表的顺序)
类型转换
1.隐式类型转换
标准的转换,经常在不经意间就发生了,如int类型和float类型相加时,int类型就会被隐式的转换位float类型,再进行相加运算。
2.显式类型转换
C++中有四个类型转换符:static_cast、dynamic_cast、const_cast和reinterpret_cast
static_cast <type-id> (expression)
expression转换为type-id类型,要用于非多态类型之间的转换,不提供运行时的检查来确保转换的安全性。
(1)进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换时,由于没有动态类型检查,所以是不安全的
(2)用于基本数据类型之间的转换,如把int转换成char,把int转换成enum等等,这种转换的安全性需要程序员来保证
(3)把void指针转换成目标类型的指针,是及其不安全的
dynamic_cast <type-id> (expression)
将expression转换为type-id类型,type-id必须是类的指针、类的引用或者是void *;如果type-id是指针类型,那么expression也必须是一个指针;如果type-id是一个引用,那么expression也必须是一个引用。主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
(1)最简单的上行转换。比如B继承自A,B转换为A,进行上行转换时,是安全的。
(2)多重继承之间的上行转换。C继承自B,B继承自A,这种多重继承的关系。
(3)转换成void *。但是,在类A和类B中必须包含虚函数,因为类中存在虚函数,就说明它有想让基类指针或引用指向派生类对象的情况,此时转换才有意义。由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表。
如果expression是type-id的基类,使用dynamic_cast进行转换时,在运行时就会检查expression是否真正的指向一个type-id类型的对象,如果是,则能进行正确的转换,获得对应的值;否则返回NULL,如果是引用,则在运行时就会抛出异常。
const_cast <type-id> (expression)
用于将类型的const、volatile和__unaligned属性移除。常量指针被转换成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然引用原来的对象。
!!!不能直接对非指针和非引用的变量使用const_cast操作符去直接移除const、volatile和__unaligned属性
reinterpret_cast <type-id> (expression)
允许将任何指针类型转换为其它的指针类型。主要用于将一种数据类型从一种类型转换为另一种类型。可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针,在实际开发中,先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原来的指针值;;特别是开辟了系统全局的内存空间,需要在多个应用程序之间使用时,需要彼此共享,传递这个内存空间的指针时,就可以将指针转换成整数值,得到以后,再将整数值转换成指针,进行对应的操作
虚函数
编译器会为每个有虚函数的类创建一个虚函数表,虚函数表被该类的所有对象共享。类的每个虚函数占据虚函数表中的一块。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。析构函数需要是虚函数,否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,从而造成内存泄露或达不到预期结果。
一般来说不要把虚析构函数写成纯虚析构函数,需要时得有定义
虚函数中的const:因为不确定子类会不会更改数据,所以虚函数最好不要声明成const了
虚函数的作用主要是实现了多态的机制。也即是被virtual修饰的函数
(1)为什么父类的析构函数必须是虚函数?
当我们动态申请一个子类对象时,使用基类指针指向该对象,如果不用虚函数,子类的析构函数不能得到调用,也就是为了在释放基类指针时可以释放掉子类的空间,防止内存泄漏.
(2)为什么C++默认的析构函数不是虚函数?
因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
内联函数
inline是C++语言中的一个关键字,可以用于程序中定义内联函数,内联函数是C++中的一种特殊函数,它可以像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。一般来说inline用于定义类的成员函数。
inline适用的函数有两种,一种是在类内定义的成员函数,另一种是在类内声明,类外定义的成员函数
(1)类内定义成员函数
在类内定义函数时,可以不加inline关键字,编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)设置为内联,具有内联函数调用的性质。
(2) 类内声明函数,在类外定义函数
这种情况下如果想将该函数设置为内联函数,则可以在类内声明时不加inline关键字,而在类外定义函数时加上inline关键字
class temp{
public:
int amount;
//普通成员函数,在类内声明时前面可以不加inline
void print_amount()
}
//在类外定义函数体,必须在前面加上inline关键字
inline void temp:: print_amount(){
cout << amount << endl;
}
如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数(使用内联函数,只不过是向编译器提出一个申请,编译器可以拒绝你的申请)
宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的
预定义宏函数的问题:
没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围
第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。和实际的函数调用不一致
第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类的成员函数
1.不能存在任何形式的循环语句
2.不能存在过多的条件判断语句
3.函数体不能过于庞大
4.不能对函数进行取址操作
段错误
访问不存在的内存地址;访问系统保护的内存地址;访问只读的内存地址;栈溢出
堆和栈
栈是向低地址扩展的数据结构,是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的,在Window下,大小是2MB,Linux下,默认为8MB。
堆是向高地址生长的,不连续,速度相对较慢,也容易内存泄漏,但获得空间大,更灵活。
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。栈则不会存在这个问题,因为栈是先进后出的队列。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长
分配方式:堆都是动态分配的。栈有2种分配方式:静态分配和动态分配。
系统响应方面:堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的 释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
内存分布
栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数
堆:内存使用new进行分配使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收
自由存储区:使用malloc进行分配,使用free进行回收。和堆类似
全局/静态存储区:全局变量和静态变量被分配到同一块内存中
常量存储区:存储常量,不允许被修改
malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或失败,new运算符其实是operator new函数的调用,它底层调用的也是malloc来开辟内存的,new它比malloc多的就是初始化功能,对于类类型来说,所谓初始化,就是调用相应的构造函数。
疑问一:堆和自由存储区是不是同一块区域
自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
int b;
//main.cpp
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main(){int b; //栈
char s[] = "abc"; // 栈
char *p2; //栈
char *p3 = "123456"; // 123456/0在常量区,p3在栈上。
static int c = 0; // 全局(静态)初始化区
p1 = (char *)malloc(10)
p2 = (char *)malloc(20) // 分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); // 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}