C++重难点知识点总结

1.C++中class和struct

C++ 中保留了C语言的struct 关键字,并且加以扩充。在C语言中,struct 只能包含成员变量,不能包含成员函数。而在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
    C++中的 struct 和class 基本是通用的,唯有几个细节不同:

使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。

class 继承默认是 private 继承,而 struct 继承默认是 public 继承.

class 可以使用模板,而 struct 不能。

       (实践证明在C++中struct声明时,可以不带任何成员,此时sizeof()为1,这一点和class一样,但是在C语言中struct声明时只要要包含一个成员,否则会报错。)

2.哪些函数不能声明成虚函数

常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。

(1)为什么C++不支持普通函数为虚函数?

普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。

多态的运行期行为体现在虚函数上,虚函数通过继承方式来体现出多态作用,顶层函数不属于成员函数,是不能被继承的

(2)为什么C++不支持构造函数为虚函数?

这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)

构造函数不能被继承,因而不能声明为virtual函数

构造函数一般是用来初始化对象,只有在一个对象生成之后,才能发挥多态

作用,如果将构造函数声明为virtual函数,则表现为在对象还没有生成的情况下酒使用了多态机制,因而是行不通的。

(3)为什么C++不支持内联成员函数为虚函数?

其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)

inline函数和virtual函数有着本质的区别,inline函数是在程序被编译时就展开,在函数调用处用整个函数体去替换,而virtual函数是在运行期才能够确定如何去调用的,因而inline函数体现的是一种编译期机制,virtual函数体现的是一种运行期机制。此外,一切virtual函数都不可能是inline函数。

(4)为什么C++不支持静态成员函数为虚函数?

这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。不能被继承,只属于该类。

(5)为什么C++不支持友元函数为虚函数?

因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。友元函数不属于类的成员函数,不能被继承。

3.内存四区理解

//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"优化成一个地方。       

}

栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

常量区—常量字符串就是放在这里的,直到程序结束后由系统释放。

代码区—存放函数体的二进制代码。

4.堆内存管理:智能指针与垃圾回收

野指针:一些内存单元已经被释放,之前指向它内存的指针却还在使用。这些内存有可能被运行时的系统重新分配给程序使用,从而导致了不可预知的错误。

重复释放:程序试图去释放已经释放过的内存单元,或者释放已经被重新分配过的内存单元,就会导致重新释放错误。通常重复释放内存会导致C/C++运行时系统打印出大量错误及诊断信息。

内存泄漏:不需要使用的内存单元如果没有被释放就会导致内存泄漏。如果程序不断的重复这类操作,将会导致内存占用剧增。

智能指针:unique_ptr、shared_ptr、weak_ptr

垃圾回收方式:(1)基于引用计数的垃圾回收器(2)基于跟踪处理的垃圾回收器

5. 类的成员函数后面加 const

类的成员函数后面加const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立。

6.多线程编程

传统的UNIX 已支持多线程的概念。每个进程都包含一个线程,因此对多个进程进行编程即是对多个线程进行编程。但是,进程同时也是一个地址空间,因此创建进程会涉及到创建新的地址空间。创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对于在进程之间切换,在线程之间进行切换所需的时间更少,因为后者不包括地址空间之间的切换。在进程内部的线程间通信很简单,因为这些线程会共享所有内容,特别是地址空间。所以,一个线程生成的数据可以立即用于其他所有线程。

7.进程间通信(IPC)方式

(1)管道pipe 半双工  只能由相关进行使用

(2)有名管道FIFO 全双工  不相关进行也能交换数据

(3)信号  信号不但能从内核发往一个进程,也能从一个进程发往另一个进程。信号本身不能直接携带信息,这就限制了它作为一项通用的进程通信机制。

(4)消息队列:系统内核中用来保存消息的队列,不是简单的FIFO,而是更为灵活。

(5)共享内存:将信息直接映射到内存,多个进程共享。通信方式最快。

8.C++中函数的参数和返回值存在三种方式进行传递,值传递、引用传递、指针传递。

(1)值传递是指将要传递的值作为一个副本传递,值传递过程中被调函数的形参作为被调函数的局部变量处理,在内存的堆栈中开辟空间以存放由主调函数放进来的实参的值,从而成为一个实参的副本。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行的,不会更改主调函数中实参变量的值。

(2)引用传递传递的是引用对象的内存地址。在地址传递的过程中,被调函数的形参也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。所以被调函数对形参做的任何操作都将影响主调函数中的实参变量。

9.指针和引用

指针和引用都是地址的概念,指针指向一块内存,它的内容是所指内存的地址,而引用某块内存的别名。指针是作为一个真正的实体而存在的。

10.内联(inline)函数

在类声明的内部声明或定义的成员函数叫做内联函数。目的是为了解决程序中函数调用的效率问题。一般来说适用于优化小的,只有几行的而且经常被调用的函数。内联函数内部不允许使用循环语句和开关语句。递归函数也不能适应于内联函数,只适用一般是1~5行的小函数。

11.C++支持函数参数不确定

当不知道有多少个参数或者只知道其中的一个或几个,通过隐藏参数…代替,调用时只需要处理所知道的参数。

12.数组名和指针的关系和区别

数组名只是一个符号,不是一个变量,因此不能作为一个左值,因此不能被修改。数组名和指针的本质区别:指针是一个变量,有自己对应的存储空间,而数组名仅仅是一个符号,不是变量,因而没有自己对应的存储空间,到此已经可以得出结论,“数组名永远不等于指针”。

13.单例模式(Singleton)

单例模式比较简单、比较常见、最容易实现。在基于对象的设计中我们通过创建一个全局对象来实现创建一个唯一的对象。Singleton模式不能够实例化,因此将其构造函数声明为protected或者直接声明为private。

  单例模式应用的场景一般发现在以下条件下:

  (1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。

  (2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

14.sizeof和strlen的联系与区别

(1)sizeof

sizeof(...)是运算符,在头文件中typedef为unsignedint,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:

数组——编译时分配的数组空间大小;

指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);

类型——该类型所占的空间大小;

对象——对象的实际占用空间大小;

函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。

(2)strlen

strlen(...)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。

它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL。

15.变量声明与变量定义的联系与区别

从广义的角度来讲声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义,例如:int a;它既是声明,同时又是定义。然而对于 extern a 来讲它只是声明不是定义。一般的情况下我们常常这样叙述,把建立空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。很明显我们在这里指的声明是范围比较窄的,即狭义上的声明,也就是说非定义性质的声明。

我们声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用就没有单独声明的必要,变量是如此,函数也是如此,所以声明不会分配存储空间,只有定义时才会分配存储空间。

用static来声明一个变量的作用有二:

(1)对于局部变量用static声明,则是为该变量分配的空间在整个程序的执行期内都始终存在。

(2)外部变量用static来声明,则该变量的作用只限于本文件模块。

程序设计风格:

1. 不要把变量定义放入.h文件,这样容易导致重复定义错误。

2. 尽量使用static关键字把变量定义限制于该源文件作用域,除非变量被设计成全局的。

3. 可以在头文件中声明一个变量,在用的时候包含这个头文件就声明了这个变量。

总结:

变量在使用前就要被定义或者声明。

在一个程序中,变量只能定义一次,却可以声明多次。

定义分配存储空间,而声明不会。

16.深度优先遍历和广度优先遍历

深度优先遍历的例子:求二叉树的深度

广度优先遍历:二叉树按层次遍历

17.类中的static函数以及static声明的变量的用法与用途

.h文件中进行类的声明,在.cpp文件中对static变量初始化一个值,这个属性只供类本身,提供给所有对象共享。static函数也是一样,不属于对象,而是类本身。使用时使用类的作用域调用即可。

18.C/C++处理常量字符串

为了节省内存,C/C++把常量字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。但使用常量字符串初始化数组,情况却不同。

19.C++程序的生与死

(1)对于全局对象,程序一开始,其构造函数就先被执行(比程序进入点更早);程序即将结束前其析构函数将被执行。

(2)对于局部对象,当对象诞生时,其构造函数被执行;当程序流程将离开该对象的声明周期时,其析构函数被执行。

(3) 对于静态(static)对象,当对象诞生时其构造函数被执行;当程序将结束时其析构函数才被执行,但比全局对象的析构函数早一步执行。

(4)对于以new方式产生出来的局部对象,当对象诞生时其构造函数被执行,析构函数则在对象被delete时执行。

静态全局对象和一般的全局对象的区别就是一般的全局对象在程序的其他文件中可以通过关键字extern来调用,而static声明的全局变量则只能在本文件中使用,这是链接性问题,一个是外部的,一个是内部的!

20.深入理解指针

int *p1 = NULL,*p2 = NULL;

int len = 10;

p2 = new int[len];

p1 = p2;

这段代码执行完之后,p1和p2都指向数组的入口地址。需要理解指针本身就是地址,并且指针变量存储的也是地址。&p1和&p2就是表示指针本身的值,因为是两个不同的变量,所以不相等。而p1和p2也是地址,是数组的入口地址,所以相等。而*p1和*P2则表示数组首元素的值。

21.构造函数和析构函数

一般情况下构造函数调用顺序是先父类,后子类;析构函数调用顺序是先子类,后父类。

class A和class B,B继承自A

 B *a = new B(); 先构造A,再构造B;先析构B类,再析构A类 

但如果是上面那种写法变成:

A *a = new B();  

delete a;  

结果就是构造函数相同,析构函数只调用A,因为a的类型是A,delete a时将调用类型A的析构函数;没析构B,造成内存泄漏。

一般遇到这样的现象,需要将基类的析构函数定义为虚拟的。

当基类析构函数是虚函数时,结果是:析构函数调用先子类后父类,不过内部原理不一样。

delete a时调用类型A的析构函数步骤如下:
1,到虚函数映射表中查找A的实际析构函数;
2,发现被实例化为B的析构函数;
3,首先调用B的析构函数;
4,再调用A的析构函数---如果A的析构不是纯虚函数的话。

22.虚析构函数的作用 

总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。所以一般基类的析构函数都是虚析构函数。当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

总结一下虚析构函数的作用: 
(1)如果父类的析构函数不加virtual关键字 
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。 
(2)如果父类的析构函数加virtual关键字 
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。

23.在构造函数中调用虚函数

#include <iostream>

using namespace std;

 

class Base

{

 

public:

       Base()

       {

              print();

 

       }

       virtualvoid print()

       {

      

              cout<< "Base prinf" << endl;

       }

};

 

class Sub :public Base

{

 

public:

       Sub()

       {

              print();

       }

       virtualvoid print()

       {

 

              cout<< "Sub prinf" << endl;

       }

};

int main(void)

{

       Sub*p = new Sub();

       deletep;

       return0;

}

程序执行结果:第一行打印Baseprint,第二行打印Sub print。因为调用基类构造函数时,派生类还没有生成,所以这里面的虚函数不会发生多态。

24.进程与线程的联系与区别

联系:进程是运行中的程序,线程是进程内部的。

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

区别:进程是资源分配的基本单位,又是调度运行的基本单位。引入线程后,进程的这两个特性被分离,线程变为调度运行的基本单位。线程是进程中进行运算的最小单位,亦即执行处理机调度的最小单位。

引入线程的好处:

(1)易于调度。

(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。

(3)开销少。创建线程比创建进程要快,所需开销很少。。

(4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

25.socket编程的步骤

客户端:socket()、connect()、write()、read()、close()

服务端:socket()、bind()、listen()、accept()、write()、read()、close()

26.虚函数指针

(1)派生类重新实现虚函数。

基类和派生类的虚函数表不同,虚函数的地址也不同。因为派生类中重新实现了虚函数,基类肯定有自己的虚函数表,虚函数的地址自然是重新实现的那个。(2)派生类不重新实现虚函数,

派生类尽管没有实现虚函数,派生类还是有自己的虚函数表,但是虚函数表中,虚函数的地址和基类中虚函数的地址一样。

基类和子类都有自己的一个虚函数表,所以无论基类和子类总共有几个虚函数,大小都是占据四字节。

27.多态

多态性可分为两类:静态多态和动态多态。函数重载和运算符重载实现的多态属于静态多态,动态多态性是通过虚函数实现的。

28.虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。(典型的菱形继承问题)虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类。

29.定义一个宏实现函数返回较小的值

#define Min(a,b)((a)<=(b)?(a):(b))

注意此处的a和b之所以用括号圈起来,是因为当a和b是表达式时,会返回一个结果,但是使用宏会直接展开代码,没有括号容易受操作符优先级的影响。

30.inline函数和宏定义的函数的作用

(1)内联函数在运行时可调试,而宏定义不可以;

(2)编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会; 

(3)内联函数可以访问类的成员变量,宏定义则不能; 

31.位运算

已知intnum;以下四种情况均能说明num能被16整除

(1)  if((num/16)*16 == num)

(2)  if((num%16) == 0)

(3)if(num&0xF == 0)

(4)if((num>>4)&&(num<<4) == num)

  • 3
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值