C++基础知识汇总

1. C++语言基础

1.1 变量

声明与定义
变量定义:用于为变量分配存储空间,还可为变量指定初始值。变量声明:用于向程序表明变量的类型和名字。对变量来说,除非有extern关键字,否则都是变量的定义。在一个程序中,变量只能定义一次,却可以声明多次。
函数的声明和定义区别比较简单,带有{ }的就是定义,否则就是声明。

如果局部变量和全局变量重名,局部变量会屏蔽全局变量。要使用全局变量,要在变量名前添加“::”。

extern关键字
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
extern “C”表示接下来的内容按C的规则进行编译。比如C++编译器通过修改所有函数的名字实现函数重载机制,C则是按函数原本的名字进行编译的。

全局变量和局部变量的存储方式
全局变量储存在静态数据库,局部变量在堆栈。全局变量在程序开始执行时分配存储区,程序执行完毕释放,在程序执行过程中全局变量始终占据固定的存储单元;局部变量是动态分配存储空间的,在调用变量所在函数时,系统会给函数的局部变量分配存储空间,在函数调用结束时就自动释放这些存储空间。

一个正数的补码与其原码的形式相同,一个负数的补码是将该数绝对值的二进制按位取反再加1.

八进制整常数必须以0开头,数码取值为0~7;十六进制整常数是以0x开头的,其数码取值为0~9,a~f;十进制整常数无前缀。

转义字符是特殊的字符常量,“\”后面接一个或几个字符,整体表示一个转义字符,例如“\n”是一个字符,表示回车。

数据类型在运算过程中的转换规则
在整型、实型和字符型数据间进行混合运算时,应从低精度向高精度转换,即将字符型数据先转换成整型,再将整型数据和实型数据转换成双精度类型数据,然后在同类型数据间进行运算。

无符号类型只能存放不带符号的整数,不能存放负数,当为其赋值为负数时会自动转换为无符号类型数值。

逗号运算符:先计算左边的操作数,再计算右边的操作数,右边操作数的类型和值作为整个表达式的结果。

左值与右值
在 c 中,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),一般指表达式结束后依然存在的持久对象。
右值指的则是只能出现在等号右边的变量(或表达式)。一般指表达式结束就不再存在的临时对象。
对于基础类型,右值是不可被修改的,也不可被 const, volatile 所修饰;但对于C++中自定义的类型(user-defined types),右值却允许通过它的成员函数进行修改。

死循环
当程序要求反复执行,然后等待中断或外界的变量的发生。如操作系统、WIN32 程序、嵌入式系统软件、多线程程序的线程处理函数等这些都用到死循环。

1.2 static、const和sizeof

static关键字
(1)全局静态变量
在全局变量前加上关键字 static, 全局变量就定义成一个全局静态变量.
存储在静态存储区, 在整个程序运行期间一直存在。
全局静态变量在声明他的文件之外是不可见的,可以被模块内所有函数访问,但不能被模块外其他函数访问。
(2)局部静态变量
在局部变量之前加上关键字 static, 局部变量就成为一个局部静态变量。
存储在静态存储区
作用域仍为局部作用域, 当定义它的函数或者语句块结束的时候, 作用域结束。 但是当局部静态变量离开作用域后, 并没有销毁, 而是仍然驻留在内存当中, 只不过我们不能再对它进行访问, 直到该函数再次被调用, 并且值不变;
(3) 静态函数
在函数返回类型前加 static, 函数就定义为静态函数。
函数的定义和声明在默认情况下都是 extern 的, 但静态函数只是在声明他的文件当中可见, 不能被其他文件所用。函数的实现使用 static 修饰, 那么这个函数只可在本 cpp 内使用, 不会同其他 cpp 中的同名函数引起冲突;
(4)类的静态成员
静态成员是类的所有对象中共享的成员, 而不是某个对象的成员。 对多个对象来说, 静态数据成员只存储一处, 供所有对象共用
(5)类的静态函数
在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this指针,因而只能访问类的 static 成员变量。

全局变量、静态全局变量、静态局部变量、局部变量的区别
存储区域:全局变量、静态全局变量、静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。
作用域:全局变量在整个工程文件内都有效,其他的源文件也能访问;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,函数返回后失效。

const关键字
(1)对变量来说
const 修饰的是只读变量,只读变量的值在定义后就不能再改变了,而且必须进行初始化。
(2)对指针来说
可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为 const;
const指针不能指向别的地址,但是可以修改其所指对象的内容,指向const对象的指针,所指内容不能改变,但可以指向别的地址。const对象的地址只能赋给const对象的指针。
(3)在一个函数声明中
const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数
若指定其为 const 类型,则表明其是一个常函数,不能修改类的成员变量;可以指定成员函数的返回值为 const 类型,以使得其返回值不为"左值",返回值也要相应地赋给一个常量或者常量指针。

const 与#define 相比有何优点
(1)const 修饰的只读变量具有特定的类型,而宏没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
(3)在C++中,编译器通常不为普通 const 只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高,而在C中,const总是占用内存。在C++中只是用const常量而不使用宏常量,即const常量完全取代宏常量。

sizeof关键字
sizeof 表示的是计算对象所占内存空间的大小,实际上它只是关键字并非函数。sizeof 在计算变量所占空间大小时,括号是可以省略的,而在计算类型大小时括号则不能省略。

sizeof 与 strlen的区别
(1)sizeof是运算符,strlen是函数;
(2)sizeof可以用类型、函数作为参数,strlen只能用char*作为参数;
(3)数组做sizeof的参数不退化,传递给strlen就退化为指针;
(4)strlen用于计算字符串的长度,而不是所占内存大小。内部实现是用一个循环计算字符串的长度,直到“\0”为止(不包括\0)。
(5)sizeof操作符的结果类型是size_t,以保证能够容纳最大对象的字节大小。

1.3 函数

传值的过程:
(1)行参与实参各占一个独立的存储空间。
(2)行参的存储空间是函数被调用时才分配的。调用开始,系统为行参开辟一个临时存储区,然后将各实参之值传递给行参,这时行参就得到了实参的值。
(3)函数返回时,临时存储区也被撤销。
传值的特点:单向传递,即函数中对行参变量的操作不会影响到调用函数中的实参变量。

传引用参数:传递的是参数的地址,所以调用后参数的值会改变。

extern “C”
C++语言支持函数重载,C 语言不支持函数重载。函数被 C++编译后在库中的名字与 C 语言的不同。C++提供了 C 连接交换指定符号 extern“C”来解决名字匹配问题。

** C 语言是怎么进行函数调用**
每一个函数调用都会分配函数栈, 在栈内进行函数执行过程。 调用前, 先把返回地址压栈, 然后把当前函数的 esp 指针压栈。参数压栈顺序是从右到左。

C++如何处理返回值:生成一个临时变量, 把它的引用作为函数参数传入函数内。

1.4 指针和引用

指针和引用的区别
(1)指针用于指向对象的地址,有自己的一块空间, 而引用只是一个别名,与对应的变量代表同一变量单元;
(2)引用必须被初始化,指针可以不用初始化;
(3)引用初始化以后就不能被改变,而指针可以改变所指向的对象;
(4)指针可以被初始化为 NULL, 而引用必须被初始化且必须是一个已有对象的引用;
(5)作为参数传递时, 指针需要被解引用才可以对对象进行操作, 而直接对引 用的修改都会改变引用所指向的对象;
(6)因为引用不能指向空值,这意味着使用引用之前不需要测试其合法性;而指针则需要经常进行测试。

引用主要是用来作为函数参数,以扩充函数传递数据的功能。使用引用传递复杂类型参数的效率更高,如果采用值传递,则从形参到实参会产生一次复制操作,而这样的复制是多余的。

系统为指针变量分配一定的内存空间,用于存储指针指向的地址,一般为一个机器字长(32位机器4字节)。

void 指针用于指向一个不属于任何类型的对象,所以 void 指针称为通用指针。void 指针可以应用于函数指针,也可以应用于纯粹的内存操作。

指针和数组
数组的名称表示数组的首地址,也就是数组第一个元素的位址。数组名+1也就表示第二个元素的地址。

指针和字符串
char str1[] = “abc”; str1表示的是字符数组的首地址。该条语句定义一个长度未知的数组,系统为后面的字符串常量分配内存,然后复制常量区的字符串,将复制后的字符串的首字母地址赋给str1.
char *str2 = “abc”; str2表示的是指向字符串常量“abc”的地址,“abc”位于常量区。字符串常量是不可更改的数据,可以使用指针指向一个字符串常量,并引用其中的数据,但是不可以更改其中的数据。大家要将字符数组和字符串常量区分开来。

指针与函数
(1)返回指针值的函数
一般定义形式为:类型名 *函数名(参数列表) 例如:int *f(int x,float y)
注意:返回的指针不能指向局部变量,因为局部变量保存在栈中,函数调用结束后释放,所以返回该地址没有意义。
(2)函数指针变量
声明的一般形式为:数据类型(*指针变量名)(参数列表);这里的数据类型指的是函数返回值的类型。
函数指针可以指向函数入口地址的值,可以通过调用函数指针来实现对函数的调用。
在给函数指针赋值的时候只需给出函数名,不用给出参数,例如p=swap; , 用函数指针变量调用函数时,只需要使用(*p)来代替函数名 swap 即可,此时要写上正确的实参。

野指针
“野指针”是在定义指针后没有对其进行初始化,或者指针指向的内存被释放,而指针没有被设置为 NULL。野指针随机的指向一个地址,使用这个指针进行操作时,就会更改该内存的数据,造成程序数据的破坏,严重威胁着程序的安全。

迷途指针
当对一个指针进行delete操作后(会释放它所指的内存),而没有把它设置为空时产生的。如果没有重新赋值就试图再次使用该指针,引起的结果是不可预料的。为了安全起见,在删除一个指针后,把它设置为空指针。

this指针
当我们调用成员函数时,实际上是替某个对象调用它。成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。编译器将对象的地址传递给this指针。
(1)this指针本质上是一个函数参数,只能在成员函数中使用,全局函数和静态函数都不能使用this,当获得一个对象后,也不能通过对象使用this指针;
(2)this指针是一个常量指针,不允许改变this保存的地址;
(4)this指针在成员函数的开始执行前构造,执行结束后清除。

2. 面向对象

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是面向对象是以功能来划分问题,把构成问题事务分解成各个对象。面向对象有三个特性,封装,继承和多态。封装就是将数据和对这些数据的操作集合在一起,并只对外暴露一些接口;继承可以让另一个类获得某一个类的属性和方法;多态则可以让同一个操作在不同的对象上表现出不同的效果。

2.1 面向对象基本概念

C 和 C++有何不同
C 语言属于面向过程语言,通过函数来实现程序功能。而 C++是面向对象语言,主要通过类的形式来实现程序功能。使可以说 C++是 C 语言的超集,它兼容 C 语言,同时扩充了许多内容。例如,面向对象、STL 模板库等。用 C++编写的面向对象应用程序比 C 语言写的程序更容易维护、扩展性更强。C++多用于开发上层的应用软件。而 C 语言代码体积小,执行效率高,多用于编写系统软件和嵌入式开发。

C++的类和结构体的区别
在类中定义的数据成员和方法,默认是私有的,即 private 访问权限。而在结构体中,默认为共有的,即 public 访问权限。

面向对象的三个基本特征
1.封装
面向对象中的封装特性是指将客观事物抽象成类,将数据和对这些数据的操作统一到类中。例如,描述一个学生对象,它需要包含学号、专业、班级等成员,同时还要包含考试、结业等方法。封装可以隐藏实现细节,通过提供公有的访问方式提高了代码的复用性和安全性,还可以“信息隐藏”,把不该暴露的信息藏起来。
2.继承
继承是面向对象中的核心技术。它允许一个类(子类)从另一个已有类(父类)中继承,这样在子类中即使不写任何代码也能够继承父类中保护和公有成员(父类中的私有成员不能够被子类继承),程序员只需在子类中增加原有类中没有的成分,实现了代码重用。面向对象中的多态性、动态绑定技术都是在继承的基础上实现的。例如定义一个员工类,从员工类中派生出经理类和操作员类。
3.多态
多态是面向对象技术的精华。它能够利用虚函数实现动态绑定功能。即一样的一条语句,由于运行时对象的类型不同,导致其行为也不同。它的主要实现方式是定义父类对象或接口时,利用子类对象的构造函数来构建父类对象。这样在调用父类对象的某一个虚方法时,由于子类对象的不同,导致同样的语句执行行为也不同。

2.2 类的定义

成员初始化
在定义数据成员时不能直接进行初始化,为数据成员提供初始值应该放在构造函数中。常量成员要在构造函数的初始化列表部分进行初始化。

访问权限
在 C++中为了使类更好的封装对象,并且不被外界所破坏。为类成员提供了 3 种访问权限,分别为 private、protected 和 public。
private:类中的 private 成员只能够在本类中或者友元类中进行访问。
protected:类中的 protected 成员只允许本类或者子类中进行访问。如果希望该成员能够被子类继承,但是不被外界访问,可以定义 protected 成员。
public:类中的 public 成员能够在本类、子类和外界中都能够进行访问。通常,类中向用户提供的服务设计为 public 成员。

构造函数
构造函数用于创建类对象。构造函数是一个与类同名的方法,可以没有参数,有一个参数或多个参数,但是构造函数没有返回值。
如果构造函数没有参数,该函数被称为类的默认构造函数。
构造函数只有一个参数时,就变成了转换函数,当该类型的参数赋值给类对象时,将实现类型转换。可以在构造函数前使用 explicit 关键字阻止构造函数的这种转换行为。
在构造函数中如果调用其他重载的构造函数,它不会执行其他构造函数的初始化部分代码,而是执行函数体部分的代码。

析构函数
析构函数的名称为“~”加上类名,没有参数和返回值。析构函数用于在对象使用后释放占用的内容资源。当一个对象超出作用域或者用户调用 delete 运算符释放对象时,系统将自动调用析构函数。通常,我们在类的构造函数中为数据成员分配堆空间,在类的析构函数释放堆空间,这样增强了程序的健壮性,系统在对象释放时会自动调用析构函数释放资源。

构造函数和析构函数的重载
在 C++中可以定义多个构造函数,让用户根据实际需要来初始化不同的数据成员。但是只需要一个析构函数来释放类对象,因为同一类对象的内存布局是相同的。所以,构造函数是可以重载的,析构函数是不允许重载的。

拷贝构造函数和赋值函数
拷贝构造函数与类名相同,参数为一个常量引用类型参数。
拷贝构造函数在三种情况下被调用:
用一个类对象初始化另一个类对象时;类对象作为函数参数或函数返回值时被调用,用于临时构建对象。
当函数按引用方式传递,形式参数和实际参数都指向同一地址,不需要赋值临时对象,也就不需要调用拷贝构造函数了。
赋值函数参数为常量引用类型,返回值为类引用类型,用于实现对象间的直接赋值。

类和对象的关系
类是对客观事物的抽象,对象是类的实例化,我们可以为对象赋值,但是不能为类赋值。类只是告诉编译器,在定义对象时如何在内存中为对象分配空间。定义一个类并不会在内存中分配空间,只有定义对象时才会为其分配空间。

局部类
如果将类的定义放在一个函数体内,则该类被称之为局部类。对于局部类来说,它只能够在函数内部使用,函数外是无法访问局部类的。局部类中不能定义静态成员。

内联成员函数
在 C++中定义内联成员函数有两种方式,一种是在定义成员函数时使用inline 关键字。另一种是在定义成员函数时直接写出函数体。对于内联函数来说,程序会在函数调用的地方直接插入函数代码,如果函数体语句较多,因此会导致程序代码膨胀。如果将类的析构函数定义为内联函数,可能会导致潜在的代码膨胀。

静态成员
使用 static 关键字将数据成员定义为静态成员。静态成员能够被同一个类的所有对象共享,它可以作用于类上,通过类名来访问。在定义静态数据成员时,还需要在全局区域对数据成员进行初始化。
如果把静态成员函数设为私有,可以通过共有静态成员函数进行访问。

静态方法
静态方法与静态数据成员类似,它能够直接使用类名来调用,而不需要定义对象。这也限制了静态方法只能访问静态数据成员,而不能访问普通的数据成员,因为在类没有实例化之前,普通数据成员是不存在的。

2.3 重载

函数重载
在调用重载方法时,编译器会根据方法的参数个数、参数类型和方法属性(const 方法),来区分不同的方法。

const方法
在定义类的方法时,如果在方法的末尾使用 const 关键字,表示该方法为 const 方法。
const 方法的最大特点是:在 const 方法中只能够访问数据成员,而不能够修改数据成员,而且在 const 方法中也不能够调用其他的非 const 方法。在设计应用程序时应尽量使用 const 方法,这可以防止用户非法修改对象,更好的体现面向对象中的封装特性。
当需要在 const 方法中修改对象的数据成员时,可以在数据成员前使用mutable 关键字,防止出现编译错误。
常量指针对象只能访问 const 方法。

运算符重载
运算符重载就是赋予已有的运算符多重含义。C++中通过重新定义运算符,使它能够用于特定类的对象执行特定的功能,允许程序员为类的用户提供一个直觉的入口,降低理解难度,使程序更加简洁直观。

运算符重载实际上是一个函数,所以运算符的重载实际上是函数的重载。一般采用成员函数或友元函数,以便可以访问类中的私有成员。例如,对类A重载++运算符,就是:A operator++(){ 函数体 }

函数参数表中参数的个数:
若运算符被定义为全局函数,则几元运算符就有几个参数。
若运算符是成员函数,则对于一元运算符没有参数,成员函数被运算符左侧的对象调用;对于二元运算符,是一个参数,单个参数是出现在运算符右侧的那个。
例如:重载后置++运算符 A operator(int ){ 函数体 } ; 重载前置++运算符 A operator() { 函数体 }

运算符重载是有限制的,它不能改变原有运算符的优先级、结合律和运算符操作个数。

2.4 C++中的类型转换

C++中四种类型转换是: static_cast, dynamic_cast, const_cast, reinterpret_cast

1、 const_cast
用于将 const 变量转为非 const。

2、 static_cast
用于各种隐式转换, 比如非 const 转 const, void*转指针,基本数据类型之间的转换等。不提供运行时的检查来确保转换的安全性,在类层次结构中,进行向上转换是安全的,向下转换不安全。

3、 dynamic_cast
用于动态类型转换。 只能用于含有虚函数的类, 用于类层次间的向上和向下转化。 只能转指针或引用。 向下转化时,通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。如果是非法的对于指针返回 NULL, 对于引用抛异常。

4、 reinterpret_cast
几乎什么都可以转, 比如将 int 转指针, 可能会出问题, 尽量少用。

3. 继承与多态

3.1 继承

继承的优缺点
优点:
1.子类可以灵活的改变父类中的已有方法
2.能够最大限度的实现代码重用
缺点:
1.子类无法在运行时改变与父类的继承关系。
2.修改父类的某些方法,可能会影响到所有子类。因此修改父类的方法要小心,可能存在一定的负面影响。
3.继承会使系统的架构层次增多,给开发和维护带来困难。

继承方式
在从父类派生一个子类时可以有 3 种派生方式。分别为 public、private和 protected。其中 public 派生方式表示父类中的公有方法和受保护方法仍然为公有方法和受保护方法。private 派生方式表示父类中的公有方法、受保护方法在子类中都是私有的。protected 派生方式表示示父类中的公有方法、受保护方法在子类中都是受保护的。

私有继承和保护继承后,对派生类对象来说,基类的所有成员都是不可见的,即派生类对象无法访问基类成员。继承和多重继承一般指公有继承。

如果不指定public,C++默认的是私有继承。

为什么派生类能够对基类成员进行操作
类对象操作的时候在内部构造时会有一个隐性的this指针。当派生类对象创建的时候,这个this指针就会覆盖到基类的范围,所以派生类能够对基类成员进行操作。

构造函数的调用顺序
类 C 继承自类B,而类 B 又继承自类 A。当构建一个 C 对象时,将至顶向下执行基类的构
造函数,最后执行自身的构造函数。因此,将首先调用类 A 的构造函数,然后调用类 B 的构造函数,最后调用类 C 的构造函数。

初始化列表
在构造函数的初始值列表中显式地初始化成员,是直接初始化数据成员,如果在构造函数内部初始化数据成员,则该成员将在构造函数体之前执行默认初始化,然后再进行赋值操作。

如果成员是const或者引用的话,必须将其初始化。当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化,这三种情况必须使用初始化列表。数组成员是不能在初始化列表里初始化的。

成员的初始化顺序与它们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

析构函数的调用顺序
类 C 继承自类B,而类 B 又继承自类 A。当C类对象释放时,将至下向上执行析构函数。本题中将首先调用C类的析构函数,然后调用B类的析构函数,最后调用A类的析构函数。

子类与父类的关系
子类在继承基类时,通常会额外添加一些属性或方法。也就是子类除了具有基类的功能外,还添加了一些自己的功能。将子类对象赋值给基类对象完全合法的,因为基类能够访问到它所定义的方法。与之相反,将一个基类赋值给子类对象是非法的。因为子类具有基类不具备的行为。

3.2 虚函数与动态绑定

虚函数
在定义成员函数时,如果使用 virtual 关键字,则该成员函数成为了一个虚函数。虚函数采用动态绑定的机制,当调用虚函数时,它会根据运行时对象的实际类型来确定具体调用哪个函数,而不是根据对象定义时的数据类型来确定。

如果在基类中定义了一个虚函数,在子类定义了相同的函数,如果不使用 virutal 关键字,它默认也是虚函数。

虚函数采用动态绑定机制,它采用迟后编译,而模板类能够根据模板参数来动态确定数据成员或参数的类型,它也采用迟后编译。

子类继承父类之后,如果重新实现了虚函数,会替换掉虚函数表中的相应的函数指针,从而实现多态。

虚函数原理
如果类中含有虚方法,则编译器需要为类构建虚函数表,表中每一项是一个虚函数的地址。每个类对象中有一个虚表指针,指向这个虚函数表的地址。所以使用虚函数的时候会产生一个系统开销。

动态绑定
基类中定义了虚函数而子类中改写该函数,定义一个基类指针并使用子类的构造函数构建对象。当调用基类中的虚方法时,会采用动态绑定的机制,也就是根据运行时该对象的实际类型来确定具体调用哪一个方法。

重载
重载是指在同一个类中有多个同名的方法,这些方法参数类型、参数个数或者方法属性(const 属性)不同。重载的函数处于同一个类中。
在调用重载成员函数时,编译器会根据传递的参数来确定具体调用哪个函数。重载的实现是编译器根据函数不同的参数表,对同名函数的名称进行修饰,然后这些同名函数就成了不同的函数。
覆盖
覆盖是指父类中定义了一个虚方法,子类中又重新定义了该方法。通过覆盖父类的虚方法,可以实现动态绑定。成员函数的覆盖发生在父类和子类之间。
隐藏
隐藏是指子类的成员函数屏蔽了父类中的同名函数。有两种情况:一是如果子类的函数与父类的函数同名,但参数不同。此时不论有无 virtual 关键字,父类的函数将被隐藏。二是如果子类的函数与父类的函数同名,且参数也相同,但是父类函数没有 virtual 关键字。
当子类隐藏父类中的方法时,会连同父类中同名的重载方法一同隐藏,因此,子类对象无法访问父类中重载的其他方法。

析构函数为什么要设计为虚函数
对象的创建过程是首先依次调用基类的构造函数,然后调用子类的构造函数,对象的释放顺序是先调用子类的析构函数,然后依次调用父类的构造函数。定义一个基类类型的指针,调用子类的构造函数为其构建对象,当对象释放时:如果析构函数是虚函数,则先调用子类的析构函数,然后再调用父类的析构函数;如果析构函数不是虚函数,则只调用父类的析构函数,如果在子类中为某个数据成员在堆中分配了空间,便不能被正确地释放,导致内存泄露的产生。

多态的两个必要条件
1.父类需要定义虚函数,子类改写该函数实现自己的行为;
2.定义一个基类指针,调用子类构造函数构建对象。
当父类对象调用虚函数时,将根据它的运行时类型来确定调用哪个类的函数。

多态性
多态性分为静态多态性和动态多态性两种。其中静态静态性是指在编译期间确定具体执行哪一项操作,它主要是通过方法重载和运算符重载来实现的。动态多态性是指在运行时确定具体执行哪一项操作。它主要是通过虚函数来实现的。

多层继承
多层继承中,采用就近调用,如果父辈中存在相关接口则优先调用父辈接口,如果父辈中也不存在相关接口则调用祖父辈接口。

RTTI:运行时类型检查
运行时类型检查,程序能够使用基类的指针或引用来检查这些指针或引用所指对象的实际派生类别。在 C++层面主要体现在 dynamic_cast 和 typeid。虚函数表的中存放了指向 type_info 的指针。 对于存在虚函数的类型, typeid 和 dynamic_cast 都会去查询type_info。

typeid操作符,返回指针或引用所指的实际类型。如果p是指针,typeid(*p)返回p所指的派生类类型,typeid§返回基类类型。
dynamic_cast 通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

3.3 抽象类

纯虚函数
纯虚函数的定义是在定义虚函数的基础上,在虚函数末尾添加“=0”。同时纯虚函数没有函数体,也就是没有函数的实现部分。

抽象类
一个包含纯虚函数的类被称为抽象类,抽象类是不能够被实例化的。抽象类通常用于作为其他类的父类,从抽象类派生的子类如果不是抽象类,则子类必须实现父类中的所有纯虚函数。

我们定义一个描述飞机的基类(抽象类),派生出两个子类:直升机和喷气式飞机。可以定义一个全局函数,负责让传递给它的飞机起飞,该函数的参数是飞机类的引用,而实际传递给它的都是子类对象,这样就可以让所有传递给它的飞机起飞。

3.4 多继承

多继承是指一个子类能够从多个类派生,也就是它可以同时具有多个父类。

虚继承
在多继承中,子类可以同时拥有多个父类,如果这些父类还有相同的父类(祖先类),那么在子类中就会有两份祖先类。例如,类 B 和类 C 均继承于类 A,如果类 D 派生于类 B 和类 C,那么类 D 中将有两份类 A。为了防止在多继承中,子类存在重复的父类情况,可以在父类继承时使用虚继承。即在类 B 和类 C 继承类 A 时使用 virtual 关键字。

4. 预处理和内存管理

4.1 宏定义

编译器在编译代码时,首先会检查代码中是否有宏,如果有,会将宏转换为宏定义的内容,也就是进行代码替换。然后再对代码进行编译。使用宏,编译器不会进行类型检查。

含参数的宏与函数的差别
(1)宏是编译期进行的,编译器在编译程序时,首先进行预编译,也就是将代码中的宏替换为宏定义的内容。而函数是运行期进行的。
(2)程序中使用宏时不会进行参数类型检查,而函数则会进行参数类型检查。
(3)宏只是编译期进行替换,而函数会在栈中定义局部变量和函数参数。
(4)宏没有生存期、作用域之类的概念,而函数则有。

#if!defined(AFX_…_HADE_H)#define(AFX_…_HADE_H)…#endif 的作用是防止头文件被重复编译。
#if 宏用于判断一个表达式的真或假,defined 指示符表示指定的表达式是否存在,#define 用于定义一个宏,#endif 用于表示#if 宏的结束,#if 必须与#endif 成对出现。题目中条件编译的作用是判断 AFX_…HADE_H 宏是否存在,如果不存在则使用#deifine 定义宏 AFX…_HADE_H,并编译#if 与#endif 之间的代码。如果存在,则#if 与#endif 之间的代码被略过。

#include 宏用于包含文件,有两种形式:
#include<math.h>表示编译器从标准库路径查找 math.h 文件。
#include“math.h”表示编译器先查找工程路径,如果没有找到 math.h文件,再查找标准库路径。
如果引用 C 或 C++标准库中的头文件,使用第一种形式;引用自定义的头文件,使用第二种形式。

在宏定义时如果不对每个参数使用括号,在宏展开时由于运算符的优先级不同,导致结果容易出现二义性。标准形式 #define MIN(x, y) ((x)>(y)?(y):(x))

inline内联函数
inline函数一般用于比较小的,频繁调用的函数,这样可以减少函数调用带来的开销。只需要在函数返回类型前加上关键字inline,即可将函数指定为inline函数。内联函数是将代码插入调用处,以代码膨胀为代价,省去了函数调用的开销,提高了函数的执行效率。
1、inline适用情况:一个函数不断被调用;函数只有简单的几行,且函数内不包含for、while、switch语句。
2.因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,这就要求每个调用了内联函数的文件中都出现了该内联函数的定义,因此,将内联函数的定义放在头文件中实现是合适的,省去了为每个文件实现一次的麻烦。
3.inline函数仅仅是对编译器的建议,最后能否真正内联要看编译器的意思。
4.关键字inline必须与函数定义体放在一起才能使函数内联。

内联函数和宏的区别
1、内联函数在编译时展开,而宏在预编译时展开
2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
4、宏不是函数,而inline是函数
5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

4.2 内存管理

堆和栈的区别
堆和栈实际是一块物理内存,堆主要用来动态分配内存,较大的数据需要在堆中分配。从堆栈内存的低端向上分配;而栈主要存储函数参数、返回值和局部变量,是从堆栈内存的高端向下分配;堆是动态分配,比如用 new分配,在使用后需要手工释放。栈属于静态分配,在对象使用后无需手动释放内存。

内存的分配方式
内存分配主要有 3 种方式,分别为静态存储区分配、堆分配和栈分配。
其中,静态存储区是在编译时就确定的,并且在整个运行期间都存在。例如,程序中的全局变量或静态变量就存储在静态存储区中。
堆分配又称为动态分配,程序在运行的时候用malloc函数或new运算符申请任意大小的内存,要用free函数或delete运算符手动释放该内存,否则会出现内存泄露。
栈分配属于静态分配,函数参数、函数返回值等都在栈中进行分配。函数调用后会自动释放栈空间。栈分配的特点是执行效率高,但是空间有限。

malloc 函数和 realloc 函数的返回值为 void类型,也就是无符号指针类型。因此将其赋值给 char类型变量 c 时需要进行强制类型转换,即c = (char*)malloc(512);

delete 与 delete[]的区别
delete 只调用一次析构函数,通常用于释放单个对象的堆空间。delete[]会调用数组中每一个元素(类对象)的析构函数,用于释放对象数组的堆空间。

如果类的定义中,没有为成员分配堆空间。那么可以使用 delete 运算符代替 delete[]运算符来为数组对象释放空间。如果为某一个数据成员在堆中分配了空间,那么在释放数组对象时就需要使用 delete[]运算符,而不能够使用 delete 运算符。

C++中有了malloc/free,为什么还需要new/delete
malloc和free是C++/C语言的标准库函数,new和delete是C++运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够吧执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能够动态内存分配和初始化工作的运算符new,以及一个能完成清理和释放内存工作的运算符delete。
而对于内部数据类型,由于内部数据类型的对象没有构造函数与析构函数的过程,对他们来说,malloc/free与new/delete是等价的。

strcpy 与 memcpy 函数的区别
strcpy 函数在拷贝字符串时,遇到’\0’就结束拷贝,因为没有指定长度, 可能会导致拷贝越界, 造成缓冲区溢出漏洞,安全版本是 strncpy 函数。 memcpy 函数将指定大小的内存数据复制到另一个内存中,当然也可以实现字符串的复制。

5. STL

STL (Standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++程序库。它是最新的C++标准函数库中的一个子集,包括容器、算法、迭代器3个组件。
STL的基本观念就是把数据和操作分离。STL中数据由容器类别来加以管理,操作则由可定制的算法来完成。迭代器在容器和算法之间充当粘合剂,算法通过迭代器获取容器中的内容,使得任何算法都可以和任何容器进行交互运作。

迭代器
Iterator(迭代器) 模式用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
迭代器不是指针, 是类模板, 通过重载了指针的一些操作符, ->、 *、 ++、 --等, 表现的像指针。本质是封装了原生指针提供了比指针更高级的行为, 可以根据不同类型的数据结构
来实现不同的++, --等操作。

5.1 STL序列容器

标准STL序列容器有vector、list、deque和string。

vector容器
vector内部使用动态数组的方式实现的。如果动态数组的内存不够用,就要动态地重新分配, 一般是当前内存的2倍,然后把原数组的内容复制过去。所以,在一般情况下,其访问速度同一般数组,只有在重新分配发生时,其性能才会下降。它的内部使用allocator类进行内存管理,程序员不需要操作内存。

allocator类
allocator类是一种“内存配置器”,负责提供内存管理相关的服务。new将内存分配和对象构建组合在一起,delete将对象析构和内存释放组合在一起,allocator 将两个阶段操作区分开来: 内存配置有 alloc::allocate()负责, 内存释放由 alloc::deallocate()负责; 对象构造由::construct()负责, 对象析构由::destroy()负责。

list和vector的区别
vector和数组类似,它拥有一段连续的内存空间,支持随机存取,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的复制(复杂度是O(n))。另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的复制,这些都大大影响了vector的效率。
list是由数据结构中的双向链表实现的,它的内存空间可以是不连续的,因此只能通过指针来进行数据的访问,不支持随机存取,需要遍历中间的元素,搜索复杂度O(n)。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。

由于vector拥有一段连续的内存空间,能非常好的支持随机存取,因此vector:iterator支持“+"、“+="、"<“等操作符。由于list的内存空间可以是不连续,它不支持随机访问,因此list:iterator不支持“+”、“+="、“<"等操作符运算。

vector和deque的区别
deque是双端队列,比vector多了push front()和pop_front()这两对首部进行操作的函数。
deque使用不止一块内存,而是有多个连续内存块构成。当需要在deque的前端或尾端增加新空间时,就会分配一段定量的连续空间,并把它接在整个deque的前端或尾端。因此不存在容量的概念,也就没有capacity()和reserve()成员函数。而对于vector来说,如果有大量的数据需要push back,应当使用reserve()函数先设定其容量大小,否则会出现许多次容量扩充操作,导致效率低下。

resize和reserve
resize改变的是size值,变小则后面的元素舍弃掉,变大则后面的元素默认初始化;
reserve改变当前容器的最大容量capacity,不增加元素。如果 reserve(len)的值大于当前的 capacity(), 那么会重新分配一块能存 len 个对象的空间, 然后把原来的元素复制过来, 销毁之前的内存。

5.2 STL适配容器

stack适配器,它可以将任意类型的序列容器转换为一个堆栈,一般使用deque作为支持的序列容器。元素只能后进先出。

queue适配器,它可以将任意类型的序列容器转换为一个队列,一般使用deque作为支持的序列容器。元素只能先进先出。

priority_queue适配器,它可以将任意类型的序列容器转换为一个优先级队列,一般使用vector作为底层存储方式。只能访问第1个元素,不能遍历整个priority queue,第1个元素始终是优先级最高的一个元素。

因为queue是先进先出,入队(调用push)是对队尾进行操作,而出队(调用pop)是对队首进行操作,deque有push_back和pop_front函数,而vector只有push_back函数,所以使用deque作为其序列容器。
stack是后进先出,入栈和退栈都是对尾部进行操作,而vector有相应的push_back和pop_back成员函数,所以可以使用vector作为stack的序列容器。

5.3 STL关联容器

关联容器中元素的排列顺序由容器的排序规则决定,并且被插入的元素并没有一个固定的位置,容器会自动按照某种排序规则将新来的元素放置在合适的位置。

其中标准的关联容器如下所示:

set容器:其中元素的值是惟一的。集合中的元素按一定的顺序排列,并被作为集合中的实例。

multiset容器:和set容器相似,然而其中的值可以重复。

map容器:map容器中存放的每一个元素都是一个键值对(pair型), 提供一对一的数据处理能力。map容器内部自建一棵红黑树,这棵树对数据自动排序,所以在map内部所有的数据都是有序的。

multimap容器:和map容器相似,然而其中的键值可以重复。

map容器和hashmap容器的区别
(1)底层数据结构不同,map是红黑树,hashmap是哈希表。
(2)map的优点在于元素可以自动按照键值排序,而hashmap的优点在于它的各项操作的平均时间复杂度接近常数。

map 和 set 的区别
map 和 set 都是 C++的关联容器, 其底层实现都是红黑树(RB-Tree) 。
map 和 set 区别在于:
(1) map 中的元素是 key-value(关键字—值) 对: 关键字起到索引的作用, 值则表示与索引相关联的数据; Set 与之相对就是关键字的简单集合, set 中每个元素只包含一个关键字。
(2)set 的迭代器是 const 的, 不允许修改元素的值; map 允许修改 value, 但不允许修改key。
其原因是: 如果允许修改 key 的话,那么首先需要删除该键, 然后调节平衡, 再插入修改后的键值, 调节平衡, 如此一来, 严重破坏了 map 和 set 的结构, 导致 iterator 失效, 不知道应该指向改变前的位置, 还是指向改变后的位置。
(3)map 支持下标操作, set 不支持下标操作。

迭代器作用
迭代器的主要目的是将容器和算法分离,迭代器让容器的数据访问和遍历操作一致,这样算法使用迭代器对数据进行操作和遍历,就不需要关心具体的容器类型,适用性也就更强。

STL迭代器删除元素
STL使用erase删除迭代器内容。
对于序列式容器(如vector,deque,list等),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vector,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。但是 erase 会返回下一个有效的迭代器。
对于关联容器(如map,set,multimap,multiset),删除当前的iterator,仅仅会使当前元素的迭代器失效,不会影响到下一个元素的迭代器, 所以在调用 erase 之前, 记录下一个元素的迭代器即可。这是因为map之类的容器,使用了红黑树来实现,插入,删除一个结点不会对其他结点造成影响。

释放内存
使用swap将该容器与一个空容器进行交换。

5.4 智能指针

申请的空间在函数结束时忘记释放, 造成内存泄漏。 使用智能指针可以很大程度上的避免这个问题, 因为智能指针就是一个类,当超出了类的作用域是, 类会自动调用析构函数, 析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间, 不需要手动释放内存空间。

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是C++11 支持, 并且第一个已经被 11 弃用。

1、 auto_ptr(c++98 的方案, cpp11 已经抛弃)
采用所有权模式。

2.、unique_ptr:独占指针(替换 auto_ptr)
unique_ptr 实现独占式拥有或严格拥有概念, 保证同一时间内只有一个智能指针可以指向
该对象。 不允许将一个unique_ptr的左值对象赋给另一个unique_ptr对象,但是右值可以。

3、shared_ptr:共享指针
多个shared_ptr对象可以指向同一对象,资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。 可以通过成员函数 use_count()来查看资源的所有者个数。一个shared_ptr对象销毁时并不会销毁其指向对象,而是将引用计数-1,当引用计数为0时,才真正销毁所指向的对象。

4、weak_ptr
这是一种不控制对象生命周期的智能指针,其指向一个shared_ptr管理的对象,但是它的构造和析构不会使shared_ptr的引用计数增减。设计这种智能指针的目是为了解决两个类中使用shared_ptr交叉引用造成的内存泄漏的问题。

当两个对象相互使用一个 shared_ptr 成员变量指向对方, 会造成循环引用, 使引用计数失效, 从而导致内存泄漏。

C++11新特性
C++11 最常用的新特性如下:
auto 关键字: 编译器可以根据初始值自动推导出类型。 但是不能用于函数传参以及数组类型的推导
nullptr 关键字: nullptr 是一种特殊类型的字面值, 它可以被转换成任意其它的指针类型;而 NULL 一般被宏定义为 0, 在遇到重载时可能会出现问题。
智能指针: C++11 新增了 std::shared_ptr、 std::weak_ptr 等类型的智能指针, 用于解决内存管理的问题。
初始化列表: 使用初始化列表来对类进行初始化
新增 STL 容器 array 以及 tuple,unordered_map。

  • 6
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值