C++知识点详细总结

  • 零、计算机基础

  • 1. C/C++内存有哪几种类型?

  • C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。

  • 全局变量、static变量会初始化为缺省值,而堆和栈上的变量是随机的,不确定的。

  • 2. 堆和栈的区别?

  • 1).堆存放动态分配的对象——即那些在程序运行时动态分配的对象,比如 new 出来的对象,其生存期由程序控制;

  • 2).栈用来保存定义在函数内的非static对象,如局部变量,仅在其定义的程序块运行时才存在;

  • 3).静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用之前分配,程序结束时销毁;

  • 4).栈和静态内存的对象由编译器自动创建和销毁。

  • 3. 堆和自由存储区的区别?

  • 总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。

  • 从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。

  • 4. 计算机内部如何存储负数和浮点数?

  • 负数比较容易,就是通过一个标志位和补码来表示。

  • 拓展问题:

  • 什么是补码?

负数补码为反码加1

正数补码为原码

负数为什么用补码?

统一加减法,正负零问题

对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。

  • 无论是单精度还是双精度在存储中都分为三个部分:

  • 1). 符号位(Sign) : 0代表正,1代表为负

  • 2). 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储

  • 3). 尾数部分(Mantissa):尾数部分

其中float的存储方式如下图所示:

C++知识点详细总结_第1张图片

而双精度的存储方式如下图:

C++知识点详细总结_第2张图片
  • 5. 左值和右值

  • 不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。举例来说我们定义的变量 a 就是一个左值,而malloc返回的就是一个右值。或者左值就是在程序中能够寻址的东西,右值就是一个具体的真实的值或者对象,没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,比如自己定义的class, 可以通过它的成员函数来修改右值。

  • 归纳一下就是:

可以取地址的,有名字的,非临时的就是左值

不能取地址的,没有名字的,临时的,通常生命周期就在某个表达式之内的就是右值

但是到了 C++11 之后概念变的略微复杂,引入了 lvalue, glvalue, rvalue, xvalue 和 prvalue。具体可以参考 What are rvalues, lvalues, xvalues, glvalues, and prvalues?

  • 6. 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?

  • 用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。

  • 1). 使用的时候要记得指针的长度.

  • 2). malloc的时候得确定在那里free.

  • 3). 对指针赋值的时候应该注意被赋值指针需要不需要释放.

  • 4). 动态分配内存的指针最好不要再次赋值.

  • 5). 在C++中应该优先考虑使用智能指针.

一.static关键字的作用

  • 1.修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。

  • 2.修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。

  • 3.修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。

  • 4.修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

  • 5.静态使用时的注意点:

a.静态方法只能访问静态成员(包括成员变量和成员方法)

b.非静态方法可以访问静态也可以访问非静态

c.静态方法中不可以定义this,super关键字

d.因为 一个类中,一个static变量只会有一个内存空间,虽然有多个类实例,但这些类实例中的这个static变量会共享同一个内存空间。静态方法在优先于对象存在,所以静态方法中不可以出现this,super关键字。

e.主函数是静态的。程序运行的时候,静态成员已经加载在内存里面了,但是包含静态成员的对象共享这些静态成员,比方说,A有一个静态成员public static int i;那么程序运行的时候,这个i就加载进内存了,A的所有对象的i变量都指向这个静态空间的i,也就是说创建对象之前,它就占空间了。

  • 只有类的成员函数才能说明为虚函数;

  • 静态成员函数不能是虚函数;

  • 内联函数不能为虚函数;

  • 构造函数不能是虚函数;

  • 析构函数可以是虚函数,而且通常声明为虚函数

二.C++和C的区别

  • 设计思想上:

C++是面向对象的语言,而C是面向过程的结构化编程语言

  • 语法上:

C++具有重载、继承和多态三种特性

C++相比C,增加多许多类型安全的功能,比如强制类型转换

C++支持范式编程,比如模板类、函数模板等

三.c++中四种cast转换

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

1.const_cast:对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。

2.static_cast:完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换。

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

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

四.C/C++ 中指针和引用的区别?

  • 1.指针有自己的一块空间,而引用只是一个别名;

  • 2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

  • 3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;

  • 4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

  • 5.可以有const指针,但是没有const引用;

  • 6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

  • 7.指针可以有多级指针(**p),而引用只有一级

  • 8.指针和引用使用++运算符的意义不一样;

  • 9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露

五.c++中的四个智能指针: shared_ptr,unique_ptr,weak_ptr,auto_ptr

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

  • 1.auto_ptr(c++98的方案,c++11已经抛弃)原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。

  • 2.unique_ptr(替换auto_ptr):是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。unique_ptr 用于取代 auto_ptr

  • 3.shared_ptr:shared_ptr实现共享式拥有概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。

  • 4.weak_ptr:weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题。

  • 智能指针使用注意事项:

  • 不使用相同的内置指针值初始化,或reset多个智能指针

  • 不delete get()返回的指针

  • 不使用get()初始化或reset另一个智能指针

  • get()返回的智能指针可能变成dangling pointer

  • 如果智能指针管理的内存不是new出来的,需要提供删除器

  • 拓展问题

  • shared_ptr 是否线程安全?

  • shared_ptr本身不是线程安全的,他只能保证不存在数据竞争的情况下,保证析构线程安全;

  • 另外shared_ptr并没有给管理的数据提供任何形式的同步,管理的数据是线程安全的,通过shared_ptr也是安全的,管理的数据不是线程安全,那就不是线程安全的

  • 侵入式智能指针?

六.野指针

  • 野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

七.为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数

  • 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

  • C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

八.函数指针

  • 1.定义:函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

  • 2.用途:调用函数和做函数的参数,比如回调函数。

  • 3.示例:

char * fun(char * p)  {…}       //  指针函数fun
char * (*pf)(char * p);             //  函数指针pf
pf = fun;                        // 函数指针pf指向函数fun
pf(p);                        // 通过函数指针pf调用函数fun

九.fork函数的作用

  • Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用,如下所示

#include 
#include 
pid_t fork(void);

成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。

最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。

十.C++中析构函数的作用

  • 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。

  • 析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。

  • 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作,所以许多简单的类中没有用显式的析构函数。

  • 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显式构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

  • 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

十一.静态函数和虚函数的区别

  • 静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

十二.重载和重写

  • 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中

  • 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

十三.虚函数和多态

  • 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。

  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

十四.下面四个代码的区别:

const char * arr = “123”; char * brr = “123”; const char crr[] = “123”; char drr[] = “123”;

const char * arr = “123”; // 字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
char * brr = “123”; // 字符串123保存在常量区,这个brr指针指向的是同一个位置,同样不能通过brr去修改"123"的值
const char crr[] = “123”; // 这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
char drr[] = “123”; // 字符串123保存在栈区,可以通过drr去修改

十五.const修饰成员函数的目的是什么?

  • const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

十六.C++里是怎么定义常量的?常量存放在内存的哪个位置?

  • 对于局部常量,存放在栈区;对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;字面值常量,比如字符串,放在常量区。

十七.new/delete与malloc/free的区别是什么

  • 首先,new/delete是C++的关键字,而malloc/free是C语言的库函数;后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数。

十八.虚函数表具体是怎样实现运行时多态的?

  • 子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。

十九.C语言是怎么进行函数调用的?

  • 每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。

二十.C++如何处理返回值?

  • 生成一个临时变量,把它的引用作为函数参数传入函数内。

二十一.C++中拷贝赋值函数的形参能否进行值传递?

  • 不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。

二十二.malloc与new区别

  • malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存大小,返回的指针不用强转。

二十三.fork,wait,exec函数的作用

  • 父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存;exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0;调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1。

二十四.C++中类成员的访问权限

  • C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

二十五. C++中struct和class的区别

  • 总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

  • 区别:最本质的一个区别就是默认的访问控制

  • 默认的继承访问权限。struct 是 public 的,class 是 private 的

  • struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

二十六.C++类的内部可以定义引用数据成员吗?

  • 可以,必须通过成员函数初始化列表初始化

class MyClass
{
public:
    MyClass(int &i):  a(1),   b(i){         //  构造函数初始化列表中是初始化工作
        //   在这里做的是赋值而非初始化工作
    }
private:
    const int a;
    int &b;  // 引用数据成员b,必须通过列表初始化!
};

二十七.什么是右值引用,跟左值又有什么区别?

  • 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。

  • 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

  • 右值引用和左值引用的区别:

1.左值可以寻址,而右值不可以;

2.左值可以被赋值,右值不可以被赋值,可以用来给左值赋值;

3.左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

二十八.C++源文件从文本到可执行文件经历的过程?

  • 对于C++源文件,从文本到可执行文件一般需要四个过程:

  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。

  • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件

  • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件

  • 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

二十九.include头文件的顺序以及双引号””和尖括号<>的区别?

  • include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。

  • 双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。对于使用双引号包含的头文件,查找头文件路径的顺序为:当前头文件目录、编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)、系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径;对于使用尖括号包含的头文件,查找头文件的路径顺序为:编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)、系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径。

三十.什么时候会发生段错误?

  • 段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:

  • 使用野指针

  • 试图修改字符串常量的内容

三十一.C++11有哪些新特性?

  • auto关键字:编译器可以根据初始值自动推导出类型,但是不能用于函数传参以及数组类型的推导;

  • nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。

  • 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。

  • 初始化列表:使用初始化列表来对类进行初始化

  • 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率

  • atomic原子操作用于多线程资源互斥操作

  • 新增STL容器array以及tuple

三十二.const的作用

  • 1.修饰变量,说明该变量不可以被修改

  • 2.修饰指针,分为指向常量的指针(即常量指针)和指针常量

  • 3.常量引用,经常用于形参类型,既避免了拷贝,又避免了函数对值的修改

  • 4.修饰成员函数,说明该成员函数内不能修改成员变量

const用法如下:

// 类
class A
{
private:
    const int a;                // 常对象成员,只能在初始化列表赋值
public:
    // 构造函数
    A() : a(0) { };
    A(int x) : a(x) { };        // 初始化列表
    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数、更新常成员变量
    const A a;                  // 常对象,只能调用常成员函数
    const A *p = &a;            // 常指针
    const A &q = a;             // 常引用
    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 常量指针即常指针,指针的指向可以改变,但是所存的内容不能变
    char const* p2 = greeting;     // 与const char* p2 等价
    char* const p3 = greeting;          // 指针常量,指针是一个常量,即指针的指向不能改变,但是指针所存的内容可以改变
    const char* const p4 = greeting;    // 指向常量的常指针,指针和指针所存的内容都不能改变,本质是一个常量
}
// 函数
void function1(const int Var);           // 传递过来的参数在函数内不可变
void function2(const char* Var);         // 参数为常量指针即指针所指的内容为常量不能变,指针指向可以改变
void function3(char* const Var);         // 参数为指针常量
void function4(const int& Var);          // 引用参数在函数内为常量
// 函数返回值
const int function5();      // 返回一个常数
const int* function6();     // 返回一个指向常量的指针变量即常量指针,使用:const int *p = function6();
int* const function7();     // 返回一个指向变量的常指针即指针常量,使用:int* const p = function7();

三十三.this 指针

  • this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。

  • 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。

  • 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。

  • this 指针被隐含地声明为: ClassName *const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);

  • this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。

  • 在以下场景中,经常需要显式引用 this 指针:

  • 为实现对象的链式引用;

  • 为避免对同一对象进行赋值操作;

  • 在实现一些数据结构时,如 list。

三十四.inline内联函数

  • 内联函数的特点:

  • 相当于把内联函数里面的内容写在调用内联函数处;

  • 相当于不用执行进入函数的步骤,直接执行函数体;

  • 相当于宏,却比宏多了类型检查,真正具有函数特性;

  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;

  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

  • 内联函数的使用:

// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);
// 声明2(不加 inline)
int functionName(int first, int second,...);
// 定义
inline int functionName(int first, int second,...) {/****/};
// 类内定义,隐式内联
class A {
    int doA() { return 0; }         // 隐式内联
}
// 类外定义,需要显式内联
class A {
    int doA();
}
inline int A::doA() { return 0; }   // 需要显式内联
  • 编译器对内联函数的处理步骤:

将 inline 函数体复制到 inline 函数调用点处;

为所用 inline 函数中的局部变量分配内存空间;

将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;

如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)

  • 使用内联函数的优缺点:

优点:

内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。

内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。

在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。

内联函数在运行时可调试,而宏定义不可以。

缺点:

代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。

是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

虚函数可以是内联函数吗?

虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。

内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。

inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

  • 虚函数内联使用实例如下:

#include 
using namespace std;
class Base
{
public:
    inline virtual void who()
    {
        cout << "I am Base\n";
    }
    virtual ~Base() {}
};
class Derived : public Base
{
public:
    inline void who()  // 不写inline时隐式内联
    {
        cout << "I am Derived\n";
    }
};
int main()
{
    // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
    Base b;
    b.who();
    // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
    Base *ptr = new Derived();
    ptr->who();
    // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
    delete ptr;
    ptr = nullptr;
    system("pause");
    return 0;
}

三十五.volatile关键字

volatile int i=10;
  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。

  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)

  • const 可以是 volatile (如只读的状态寄存器)

  • 指针可以是volatile

三十六.assert()

  • 断言是宏,而非函数。assert 宏的原型定义在 (C)、(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include 之前。

assert()使用
#define NDEBUG          // 加上这行,则 assert 不可用
#include 
assert( p != NULL );    // assert 不可用

三十七.sizeof()运算符

  • sizeof 对数组,得到整个数组所占空间大小。

  • sizeof 对指针,得到指针本身所占空间大小。

三十八.#pragma pack(n)

  • 用途:设定结构体、联合以及类成员变量以 n 字节方式对齐

#pragma pack(n)使用实例:
#pragma pack(push)  // 保存对齐状态
#pragma pack(4)     // 设定为 4 字节对齐
struct test
{
    char m1;
    double m4;
    int m3;
};
#pragma pack(pop)   // 恢复对齐状态

三十九. extern “C”

  • 用途:extern “C” 的作用是让 C++ 编译器将 extern “C” 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

  • 被 extern 限定的函数或变量是 extern 类型的;被 extern “C” 修饰的变量和函数是按照 C 语言方式编译和链接的

  • extern "C"实例如下:

#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif

四十.struct 和 typedef struct

  • C语言中:

// c
typedef struct Student {
    int age;
} S;
// 等价于下面
struct Student {
    int age;
} ;
typedef struct Student S;
  • C++中:

1.如果在类标识符空间定义了 struct Student {…};,使用 Student me; 时,编译器将搜索全局标识符表,Student 未找到,则在类标识符内搜索。即表现为可以使用 Student 也可以使用 struct Student,如下:

    // cpp
    struct Student {
        int age;
    };
    void f( Student me );       // 正确,"struct" 关键字可省略

2.若定义了与 Student 同名函数之后,则 Student 只代表函数,不代表结构体,如下:

 typedef struct Student {
        int age;
    } S;
    void Student() {}           // 正确,定义后 "Student" 只代表此函数
    //void S() {}               // 错误,符号 "S" 已经被定义为一个 "struct Student" 的别名
    int main() {
        Student();
        struct Student me;      // 或者 "S me";
        return 0;
    }

四十一.union联合体

  • 联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:

  • 默认访问控制符为 public

  • 可以含有构造函数、析构函数

  • 不能含有引用类型的成员

  • 不能继承自其他类,不能作为基类

  • 不能含有虚函数

  • 匿名 union 在定义所在作用域可直接访问 union 成员

  • 匿名 union 不能包含 protected 成员或 private 成员

  • 全局匿名联合必须是静态(static)的

  • union使用实例如下:

#include
union UnionTest {
    UnionTest() : i(10) {};
    int i;
    double d;
};
static union {
    int i;
    double d;
};
int main() {
    UnionTest u;
    union {
        int i;
        double d;
    };
    std::cout << u.i << std::endl;  // 输出 UnionTest 联合的 10
    ::i = 20;
    std::cout << ::i << std::endl;  // 输出全局静态匿名联合的 20
    i = 30;
    std::cout << i << std::endl;    // 输出局部匿名联合的 30
    return 0;
}

四十二.explicit(显式)关键字

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化,必须显式初始化

  • explicit 修饰转换函数时,可以防止隐式转换,但按语境转换 除外

  • explicit使用实例如下:

struct A
{
    A(int) { }
    operator bool() const { return true; }
};
struct B
{
    explicit B(int) {}
    explicit operator bool() const { return true; }
};
void doA(A a) {}
void doB(B b) {}
int main()
{
    A a1(1);        // OK:直接初始化
    A a2 = 1;       // OK:复制初始化
    A a3{ 1 };      // OK:直接列表初始化
    A a4 = { 1 };       // OK:复制列表初始化
    A a5 = (A)1;        // OK:允许 static_cast 的显式转换
    doA(1);         // OK:允许从 int 到 A 的隐式转换
    if (a1);        // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a6(a1);        // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a7 = a1;       // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a8 = static_cast(a1);  // OK :static_cast 进行直接初始化
    B b1(1);        // OK:直接初始化
    B b2 = 1;       // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
    B b3{ 1 };      // OK:直接列表初始化
    B b4 = { 1 };       // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
    B b5 = (B)1;        // OK:允许 static_cast 的显式转换
    doB(1);         // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
    if (b1);        // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
    bool b6(b1);        // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
    bool b7 = b1;       // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
    bool b8 = static_cast(b1);  // OK:static_cast 进行直接初始化
    return 0;
}

四十三.friend友元类和友元函数

  • 能访问私有成员、破坏封装性、友元关系不可传递、友元关系的单向性、友元声明的形式及数量不受限制

四十四.:: 范围解析运算符

  • 种类:

全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间

类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的

命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

  • 使用实例:

int count = 0;        // 全局(::)的 count
class A {
public:
    static int count; // 类 A 的 count(A::count)
};
int main() {
    ::count = 1;      // 设置全局的 count 的值为 1
    A::count = 2;     // 设置类 A 的 count 为 2
    int count = 0;    // 局部的 count
    count = 3;        // 设置局部的 count 的值为 3
    return 0;
}

四十五.enum枚举类型

  • 限定作用域的枚举类型:

enum class open_modes { input, output, append };
  • 不限定作用域的枚举类型:

enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };

四十六.decltype关键字

  • 作用和用法:用于检查实体的声明类型或表达式的类型及值分类。语法:decltype ( expression )

  • decltype实例如下:

// 尾置返回允许我们在参数列表之后声明返回类型
template 
auto fcn(It beg, It end) -> decltype(*beg)
{
    // 处理序列
    return *beg;    // 返回序列中一个元素的引用
}
// 为了使用模板参数成员,必须用 typename
template 
auto fcn2(It beg, It end) -> typename remove_reference::type
{
    // 处理序列
    return *beg;    // 返回序列中一个元素的拷贝
}

四十七.引用和宏

  • 左值引用:常规引用,一般表示对象的身份

  • 右值引用:右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值;右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

  • 能够更简洁明确地定义泛型函数。

  • 引用折叠: X& &、X& &&、X&& & 可折叠成 X&;X&& && 可折叠成 X&&

  • 宏:宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对 “参数” 进行的是一对一的替换。

四十八.必须使用成员初始化列表的场合

  • 好处:更高效:少了一次调用默认构造函数的过程。

  • 有些场合必须要用初始化列表:

常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面

引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化

四十九.面向对象三大特征

  • 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。

public 成员:可以被任意实体访问

protected 成员:只允许被子类及本类的成员函数访问

private 成员:只允许被本类的成员函数、友元类或友元函数访问

  • 继承:基类(父类)——> 派生类(子类)

  • 多态:即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。

  • C++ 多态分类及实现:

重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载

子类型多态(Subtype Polymorphism,运行期):虚函数

参数多态性(Parametric Polymorphism,编译期):类模板、函数模板

强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

静态多态(编译期/早绑定)

  • 函数重载实例:

class A
{
public:
    void do(int a);
    void do(int a, int b);
};
  • 动态多态(运行期/晚绑定)

虚函数:用 virtual 修饰成员函数,使其成为虚函数

  • 注意:

普通函数(非类成员函数)不能是虚函数

静态函数(static)不能是虚函数

构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)

内联函数不能是表现多态性时的虚函数

  • 动态多态实例

class Shape                     // 形状类
{
public:
    virtual double calcArea()
    {
        ...
    }
    virtual ~Shape();
};
class Circle : public Shape     // 圆形类
{
public:
    virtual double calcArea();
    ...
};
class Rect : public Shape       // 矩形类
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    Shape * shape2 = new Rect(5.0, 6.0);
    shape1->calcArea();         // 调用圆形类里面的方法
    shape2->calcArea();         // 调用矩形类里面的方法
    delete shape1;
    shape1 = nullptr;
    delete shape2;
    shape2 = nullptr;
    return 0;
}

五十.虚析构函数

  • 虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

  • 虚析构函数的使用如下:

class Shape
{
public:
    Shape();                    // 构造函数不能是虚函数
    virtual double calcArea();
    virtual ~Shape();           // 虚析构函数
};
class Circle : public Shape     // 圆形类
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    shape1->calcArea();
    delete shape1;  // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
    shape1 = NULL;
    return 0;
}

五十一.纯虚函数

  • 定义:纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

  • 用法: virtual int A() = 0;

五十二.虚函数、纯虚函数

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。

  • 虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现。

  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。当然大家也可以完成自己的实现。纯虚函数关注的是接口的统一性,实现由子类完成。

  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。

  • 虚基类是虚继承中的基类。

五十三.虚函数指针、虚函数表

  • 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。

  • 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚函数表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

五十四.虚继承

  • 用途:用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)

  • 底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

五十五.虚继承、虚函数

  • 相同点:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)

  • 不同点:

  • 虚继承:

虚基类依旧存在继承类中,只占用存储空间

虚基类表存储的是虚基类相对直接继承类的偏移

  • 虚函数:

虚函数不占用存储空间

虚函数表存储的是虚函数地址

五十六.模板类、成员模板、虚函数

  • 模板类中可以使用虚函数

一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数

五十七.抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类

  • 接口类:仅含有纯虚函数的抽象类

  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:

所有成员都是 public

没有定义任何构造函数

没有类内初始化

没有基类,也没有 virtual 函数

五十八.内存分配和管理

  • malloc、calloc、realloc、alloca

  • malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。

  • calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。

  • realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。

  • alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。

  • malloc和free

  • 用途:用于分配、释放内存

  • 使用:

申请内存,确认是否申请成功

char *str = (char*) malloc(100);
assert(str != nullptr);

释放内存后指针置空

free(p);
p = nullptr;
  • new和delete

  • new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。

  • delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。

  • new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

  • 使用:

int main()
{
    T* t = new T();     // 先内存分配 ,再构造函数
    delete t;           // 先析构函数,再内存释放
    return 0;
}

五十九.delete this 合法吗?

  • 合法,但是:

  • 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的

  • 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数

  • 必须保证成员函数的 delete this 后面没有调用 this 了

  • 必须保证 delete this 后没有人使用了

六十.如何定义一个只能在堆上(栈上)生成对象的类?

  • 只能在堆上

  • 方法: 将析构函数设置为私有

  • 原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

  • 只能在栈上

  • 方法:将 new 和 delete 重载为私有

  • 原因: 在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

六十一.强制类型转换运算符(4种)

  • static_cast

  • 特点:静态转换,在编译处理期间。

  • 应用场合: 主要用于C++中内置的基本数据类型之间的转换,但是没有运行时类型的检测来保证转换的安全性。

  • a.用于基类和子类之间的指针或引用之间的转换,这种转换把子类的指针或引用转换为基类表示是安全的;进行下行转换,把积累的指针或引用转换为子类表示时,由于没有进行动态类型检测,所以是不安全的。

  • b.把void类型的指针转换成目标类型的指针(不安全)

  • c.不能用于两个不相关的类型转换

  • d.不能把const对象转换成非const对象

  • const_cast

  • 特点:去常转换,编译时执行。

  • 应用场合: const_cast操作不能在不同的种类间转换。相反,它仅仅把它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。

  • reinterpret_cast:

  • 特点:重解释类型转换

  • 应用场合: 它有着和c风格强制类型转换同样的功能;它可以转化任何的内置数据类型为其他的类型,同时它也可以把任何类型的指针转化为其他的类型;它的机理是对二进制进行重新的解释,不会改变原来的格式。

  • dynamic_cast < type-id > ( expression )

  • 特点:该运算符将expression转换成type_id类型的对象。type_id必须是类的指针,类的引用或者空类型的指针。

  • 应用场合:

  • a.如果type_id是一个指针类型,那么expression也必须是一个指针类型,如果type_id是一个引用类型,那么expression也必须是一个引用类型。

  • b.如果type_id是一个空类型的指针,在运行的时候,就会检测expression的实际类型,结果是一个由expression决定的指针类型。

  • c.如果type_id不是空类型的指针,在运行的时候指向expression对象的指针能否可以转换成type_id类型的指针

  • d.在运行的时候决定真正的类型,如果向下转换是安全的,就返回一个转换后的指针,若不安全,则返回一个空指针

  • e.主要用于上下行之间的转换,也可以用于类之间的交叉转换。上行转换时和static_cast效果一样,下行转换时,具有检测功能,比static_cast更安全。

六十二.new delete和malloc free的联系和区别

  • malloc与free是C语言的标准库函数, new/delete是C++的运算符。它们都可用于申请动态内存和释放内存;

  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构

  • 造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之

  • 内,不能够把执行构造函数和析构函数的任务强加于malloc/free;

  • C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete,注意new/delete不是库函数。

六十三.hash冲突及解决方法

  • 关键字值不同的元素可能会映射到哈希表的同一地址上就会发生哈希冲突。解决办法:

  • 开放定址法:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。

  • 再哈希法:同时构造多个不同的哈希函数

  • 链地址法:将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个

  • 单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况

  • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

六十四.多态是什么,多态的作用?

  • 定义:同一个对象,在不同时刻体现出来的不同状态。

  • 多态的前提:

  • 要有继承关系或实现关系(接口)

  • 要有方法重写;

  • 要有父类或者父接口引用指向子类Base b= new Derived();

  • 作用:提高了代码的维护性(继承保证);提高了代码的扩展性

六十五. 继承含有纯虚函数的父类,子类能否实例化?

  • 如果父类中存在纯虚函数,子类继承父类时,必须重写父类的纯虚函数,函数名、返回类型、参数个数和类型都不能改。若父类中的虚函数自己有定义,子类也可以不重写。之后便可以实例化子类。

六十六.构造函数是否可以用private修饰,如果可以,会有什么效果?

  • 如果一个类的构造函数只有一个且为private,这是可以编译通过的;

  • 如果一个类的构造函数只有一个且是private,如果类的内部没有专门创建实例的代码,则是无法创建任何实例的;

  • 如果一个类的构造函数只有一个且是private,如果类的内部有专门创建实例的代码,则只能创建一个或多个实例(根据类内部声明的成员对象个数来定);

  • 如果一个类的构造函数不止一个,private 构造函数如果参数 为void(无参),则子类无法编译;换言之,如果一个类构造函数只有private且存在子类,则无法编译,除非父类构造函数为public。

六十七.子类的指针能否转换为父类的指针?父类指针能否访问子类成员?

  • 当自己的类指针指向自己类的对象时,无论调用的是虚函数还是实函数,其调用的都是自己的

  • 当指向父类对象的父类指针被强制转换成子类指针时候,子类指针调用函数时,只有非重写函数是自己的,虚函数是父类的;

  • 当指向子类对象的子类指针被强制转换成父类指针的时候,也就是父类指针指向子类对象,此时,父类指针调用的虚函数都是子类的,而非虚函数都是自己的;

六十八.虚函数的实现机制

  • 1、c++实现多态的方法

其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面可以知道:是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

class A
{
public:
    virtual void f();
    virtual void g();
private:
    int a
};

class B : public A
{
public:
    void g();
private:
    int b;
};

//A,B的实现省略

因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:

A::f 的地址

A::g 的地址

B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:

A::f 的地址

B::g 的地址

注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:

vptr : 指向B的虚表vtableB

int a: 继承A的成员

int b: B成员

当如下语句的时候:

A *pa = &bB;

pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)

那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始哈~)。

这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的vptr指向的是B的虚表vtableB)

另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。我以上的结果是根据g++ 4.3.4经过反汇编分析出来的。

  • 2、两种多态实现机制及其优缺点

除了c++的这种多态的实现机制之外,还有另外一种实现机制,也是查表,不过是按名称查表,是smalltalk等语言的实现机制。这两种方法的优缺点如下:

  1. 按照绝对位置查表,这种方法由于编译阶段已经做好了索引和表项(如上面的call *(pa->vptr[1]) ),所以运行速度比较快;缺点是:当A的virtual成员比较多(比如1000个),而B重写的成员比较少(比如2个),这种时候,B的vtableB的剩下的998个表项都是放A中的virtual成员函数的指针,如果这个派生体系比较大的时候,就浪费了很多的空间。

  1. 比如:GUI库,以MFC库为例,MFC有很多类,都是一个继承体系;而且很多时候每个类只是1,2个成员函数需要在派生类重写,如果用C++的虚函数机制,每个类有一个虚表,每个表里面有大量的重复,就会造成空间利用率不高。于是MFC的消息映射机制不用虚函数,而用第二种方法来实现多态,那就是:

  1. 按照函数名称查表,这种方案可以避免如上的问题;但是由于要比较名称,有时候要遍历所有的继承结构,时间效率性能不是很高。(关于MFC的消息映射的实现,看下一篇文章)

3、总结:

  • 如果继承体系的基类的virtual成员不多,而且在派生类要重写的部分占了其中的大多数时候,用C++的虚函数机制是比较好的;

  • 但是如果继承体系的基类的virtual成员很多,或者是继承体系比较庞大的时候,而且派生类中需要重写的部分比较少,那就用名称查找表,这样效率会高一些,很多的GUI库都是这样的,比如MFC,QT

你可能感兴趣的:(c++,c++)