C++学习笔记

  1. 基类私有成员,无论什么派生权限,派生类内成员函数和类外都是不可以访问的。
  2. 私有继承,无论基类原来什么类型,在派生类外通过成员函数都不可以访问。
  3. 派生类从基类中吸收的成员的访问权限为基类中访问权限和派生时派生权限两者之中最低的一种。

        根据派生的权限、基类中定义的权限,在派生类的类内和类外不同访问时的组合情况,列出下表:

公有继承 保护继承 私有继承
访问位置 类内 类外 类内 类外 类内 类外
公有成员 可以 可以 可以 不可以 可以 不可以
保护成员 可以 不可以 可以 不可以 可以 不可以
私有成员 不可以 不可以 不可以 不可以 不可以 不可以
  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限将保持不变。也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
  2. 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
  3. 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
  4. 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

        注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

        创建派生类在调用自己的构造函数之前,会先调用基类的构造函数。

        派生类和基类的构造函数会自动调用,调用顺序是先调用基类的构造函数再调用的派生类构造函数。

        注意:一旦基类中有带参数的构造函数,派生类中则必须有显式传参的派生类构造函数,来实现基类中参数的传递,完成初始化工作。

        析构函数的调用顺序与构造函数则完全相反。

        构造函数调用顺序:基类->派生类

        析构函数调用顺序:派生类->基类

        构造函数的调用顺序:

        基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:A --> B --> C;那么创建C类对象时构造函数的执行顺序为:A类构造函数-->B类构造函数--> C类构造函数。构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

        派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

        在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

        类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。

        面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。

        “多态”指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态和运行时的多态。前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关,是本章要讲述的内容。本教程后面提及的多态都是指运行时的多态。

        C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。同一条语句可以执行不同的操作,看起来有不同表现方式,这就是多态。

        构成多态的条件:

  1. 必须存在继承关系;
  2. 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  3. 存在基类的指针,通过该指针调用虚函数。

        静态联编:函数重载、函数模板的实例化

        动态联编:在运行的时候,才能确认执行哪段代码。

        静态联编由于编译时候就已经确定好怎么执行,因此执行起来效率高;而动态联编想必虽然慢一些,但优点是灵活。

        虚函数:

  1. 虚函数不能是静态成员函数,或友元函数,因为它们不属于某个对象。
  2. 内联函数不能在运行中动态确定其位置,即使虚函数在类的内部定义,编译时,仍将看作非内联,
  3. 构造函数不能是虚函数,析构函数可以是虚函数,而且通常声明为虚函数。

        C++中虚函数的唯一用处就是构成多态。

        抽象类:virtual 返回值类型 函数名 (函数参数) = 0;

  1. 抽象类无法实例出一个对象来,只能作为基类让派生类完善其中的纯虚函数,然后再实例化使用。
  2. 抽象类的派生来依然可以不完善基类中的纯虚函数,继续作为抽象类被派生。直到给出所有纯虚函数的定义,则成为一个具体类,才可以实例化对象。
  3. 抽象类因为抽象、无法具化,所以不能作为参数类型、返回值、强转类型,但抽象类可以定义一个指针、引用类型,指向其派生类,来实现多态特性。
  4. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
  5. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

        包含纯虚函数的类称为抽象类。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

        函数重载:参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数返回值也不能作为重载的依据。

        函数重载规则:

  1. 函数名称必须相同。
  2. 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
  3. 函数的返回类型可以相同也可以不相同。
  4. 仅仅返回类型不同不足以成为函数的重载。

        运算符重载格式:

返回值类型 operator 运算符名称 (形参表列){

    //TODO:

}

        运算符重载注意:

  1. 重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
  2. C++ 规定,运算符重载不改变运算符的优先级。
  3. 以下运算符不能被重载:.、.*、::、? :、sizeof。
  4. 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。

        类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。

        类本身不占用内存空间,而变量的值则需要内存来存储。

        使用new在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。

        一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。

        static成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。

        静态成员变量必须初始化,而且只能在类体外进行。

        静态成员函数与普通成员函数的根本区别:普通成员函数有this指针,可以访问类中的任意成员;而静态成员函数没有this指针,只能访问静态成员(包括静态成员变量和静态成员函数)。

        成员函数中出现的 this 指针,就是指向成员函数所作用的对象的指针。因此,静态成员函数内部不能出现 this 指针。成员函数实际上的参数个数比表面上看到的多一个,多出来的参数就是 this 指针。

        每个对象有各自的一份普通成员变量,但是静态成员变量只有一份,被所有对象所共享。静态成员函数不具体作用于某个对象。即便对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。

        常量对象上面不能执行非常量成员函数,只能执行常量成员函数。

        const 成员和引用成员必须在构造函数的初始化列表中初始化(无法在构造函数内部使用赋值方式初始化),此后值不可修改。

        引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)。

        友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。

        友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。

        友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。

        除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。

        C++中struct和class区别:

  1. 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
  2. class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
  3. class 可以使用模板,而struct不能。

        对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。

        将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。

        如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。如果类的成员变量没有指针一般浅拷贝足以。另外一种需要深拷贝的情况就是在创建对象时进行一些预处理工作,比如统计创建过的对象的数目、记录对象创建的时间等。

        网络连接也是一个文件,它也有文件描述符

        流格式套接字(SOCK_STREAM):流格式套接字(Stream Sockets)也叫“面向连接的套接字”。SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。

        TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。

        数据报格式套接字(SOCK_DGRAM):数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”。因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟。

        OSI 7层网络模型:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,所以没有大规模应用。

        TCP/IP 模型:接口层、网络层、传输层和应用层。

        程序一般都是通过应用层来访问网络的,程序产生的数据会一层一层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。数据每往下走一层,就会被这一层的协议增加一层包装,等到发送到互联网上时,已经比原始数据多了四层包装。当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。给数据加包装的过程,实际上就是在数据的头部增加一个标志(一个数据块),表示数据经过了这一层,我已经处理过了。给数据拆包装的过程正好相反,就是去掉数据头部的标志,让它逐渐现出原形。

C++学习笔记_第1张图片

        socket编程,是站在传输层的基础上,所以可以使用TCP/UDP协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的http协议位于应用层。

        sockaddr是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而sockaddr_in用来保存IPv4地址的结构体。sockaddr_in6,用来保存IPv6地址。

        listen()只是让套接字进入监听状态,并没有真正接收客户端请求,listen()后面的代码会继续执行,直到遇到 accept()。accept()会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

        每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。发送端只有在收到对方的 ACK 确认包后,才会清空输出缓冲区中的数据。

        I/O缓冲区特点:

  1. I/O缓冲区在每个TCP套接字中单独存在;
  2. I/O缓冲区在创建套接字时自动生成;
  3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  4. 关闭套接字将丢失输入缓冲区中的数据。

        TCP套接字默认情况下是阻塞模式,也是最常用的,也可以更改为非阻塞模式。

        三次握手示意图:        

C++学习笔记_第2张图片

        三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。

        四次挥手示意图

C++学习笔记_第3张图片

        TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。

        UDP是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要ACK包确认。UDP只有创建套接字的过程和数据交换的过程。

        TCP 的速度无法超越 UDP,但在收发某些类型的数据时有可能接近 UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近于 UDP。

你可能感兴趣的