笔记|《面向对象编程技术与方法(C++)》电子工业出版社

发布时间 2023-07-29 18:23:41作者: LateSpring

tb_image_share_1690624889801.jpg

第一章 概述

C++多态:
https://blog.csdn.net/K346K346/article/details/82774937

第二章 编程基础

数据类型

枚举:
https://www.runoob.com/w3cnote/cpp-enum-intro.html
联合:
https://www.runoob.com/cprogramming/c-unions.html
作用域运算符
c++入门学习篇(1)之::作用域符解析

c++ 条件编译指令

条件编译指令(符号),C语言条件编译指令完全攻略

名字空间

C++ 命名空间 | 菜鸟教程
一般不能在头文件中使用名字空间

第三章 函数

函数指针

C++ 函数指针 & 类成员函数指针 | 菜鸟教程

inline函数

inline函数一般适用于只有几行的小程序。复杂函数(如包含循环语句、开关语句、递归调用等)不能作为inline函数。

inline函数的定义应出现在被调用之前,并且在调用该函数的每个文本文件中都要进行定义。因此建议把inline 函数的定义放到头文件中,使用时只需用“#include”包含该头文件。

inline只表示一种要求, 编译器并非一定将inline修饰的函数做内嵌处理。类体内定义的函数即使不带inline关键字,也是inline函数。

使用inline函数可以节省运行时间,但程序的目标代码量会增加。

inline函数与带参数的宏具有相似的功能,但后者存在安全问题,不建议用。

第四章 类与对象

成员初始化列表

https://blog.csdn.net/XIONGXING_xx/article/details/115553291

拷贝构造函数

C++ 拷贝构造函数
拷贝构造函数何时被调用?
a.对象的直接赋值也会调用拷贝构造函数 ;
b.函数参数传递只要是按值传递也调用拷贝构造函数;
c.函数返回只要是按值返回也调用拷贝构造函数。

转换构造函数

c++ 中的转换构造函数

析构函数

在全局对象、静态局部对象、一般局部对象都存在的情况下,自动析构顺序一般是:先删除一般局部对象,其次是静态局部对象,最后删除全局对象。

用new建立的堆对象,只能通过delete删除。

函数内定义的对象,当这个函数被调用结束时删除(不包括静态对象)。

赋值成员函数

赋值函数和复制构造函数的实现非常相似。不同的是,赋值函数返回*this,以符合赋值的本来含义。复制构造函数用于初始化正在创建的同类对象,而赋值函数用于修改已经存在的类对象的值。

class A
{
public:
    A(int r){ p = new int(r); } //在构造函数中初始化p, 指向-一个int对象~A() { delete p; }
    ~A(){ delete p; }			//在析构函数中释放p所指向的内存空间
private:
    int *p;
    //指针型数据成员
};

在上面的类A中,包含一个指针型数据成员。我们没有定义复制构造函数和赋值函数,这两个成员函数将由编译系统自动产生。对于下面的语句,虽然可以通过编译、连接,但程序运行时会出现问题。

A a(5);		//建立A类的对象a,将P指向的对象初始化为5
A b(a) ;	//建立对象b,调用复制构造函数
A c(5),d(10);	//建立两个A类对象
d=c;			//调用赋值函数

在建立对象b时,调用默认的复制构造函数,用对象a初始化b,结果只是指针值的复制,造成a.p和b.p指向同一一个对象(同一段内存);在程序结束调用析构函数时,将删除同一个对象两次,即释放同一段内存两次。当调用赋值函数用对象c的值修改对象d时,结果也只是指针值的复制,即d.p被更新为c.p的值,即两个指针都指向c.p所指向的内存,造成d.p原先所指向的内存区域无法访问;执行析构函数时,将删除同一个对象两次,而另一个对象无法删除,造成内存资源泄漏。内存示意图参见下图。
image.png
为解决上述问题用户需自定义复制构造函数和赋值函数。

class A
{
public:
    A(int r){ p =new int(r); } 	//在构造函数中初始化p,使其指向一个int对象
    ~A() {delete p;};			//在析构函数中释放p所指向的内存空间
    A(const A& r){ p=new int(*r.p);} //复制构造函数
    A& operator = (const A& r)		//赋值函数
    {
        if(this == &r)
        return *this;		//如果是自引用,则返回当前对象
        delete p;			//否则先释放当前对象p所指内存
        p = new int(*r.p) ;	//使p指向新内存,用r.p所指内存的值初始化
        return *this;
    }
private :
	int *p;		//指针型数据成员
};

上面的例子在复制构造函数中,使新建对象的p指向一段新内存,并用r.p 所指内存的值初始化新内存。在赋值函数中,先判断参数是否是自引用的,如果是,则返回当前对象;如果不是首先释放当前对象的p所指的内存,然后使p指向新内存,并用r.p 所指内存的值初始化新内存,再返回当前对象。这样就可以避免采用默认构造函数和默认赋值函数所带来的问题。这时的内存示意图见下图。
image.png
如果一个类需要定义析构函数,那么几乎可以肯定它也需要定义拷贝构造函数和赋值运算符。
需要拷贝操作的类也一定需要赋值操作,反之亦然。虽然很多类需要定义所有(或是不需要定义任何)拷贝控制成员,但某些类所要完成的工作,只需要拷贝或者赋值操作,不需要析构操作。
考虑一个类为每个对象分配一个独有的、唯一的编号。这个类除了需要一个拷贝构造函数为每个新创建的对象生成一个新的编号,还需要一个赋值运算符来避免将一个对象的编号赋值给另外一个对象。但是,这个类并不一定需要析构函数。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个赋值运算符;反之亦然。然而,无论需要拷贝构造函数还是需要复制运算符,都不必然意味着也需要析构函数。

定义一个类时,我们显式地或隐式地指定了此类型的对象在拷贝、赋值和销毁时做什么。一个类通过定义三种特殊的成员函数来控制这些操作,分别是拷贝构造函数赋值运算符析构函数。拷贝构造函数定义了当用同类型的另一个对象初始化新对象时做什么,赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么,析构函数定义了此类型的对象销毁时做什么。我们将这些操作称为拷贝控制操作
由于拷贝控制操作是由三个特殊的成员函数来完成的,所以我们称此为“C++三法则”。在较新的 C++11 标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”。也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的。为了统一称呼,后来人们干把它叫做“C++ 三/五法则”。

静态成员

静态数据成员

一般地,如果定义n个同类的对象,那么每个对象都分别拥有自己的一套成员, 不同对象的数据成员各自有值,互不相关。有时我们希望某些数据成员在同类的多个对象之间可以共享,这时可以将它们定义为静态数据成员。
static关键字只在类内的静态数据成员声明时使用,静态数据成员一定要在类体外进行初始化。
静态数据成员初始化格式为:
数据类型类名::静态数据成员名=初值;
一般数据成员是在对象建立时分配空间,在对象删除时释放空间的。而静态数据成员是在程序编译时分配空间,到程序结束时释放空间的。静态数据成员在所有对象之外单独存放,而不占用对象的存储空间。
尽管静态数据成员的存储具有全局性,但其作用域仅限于所属类的范围。与普通的数据成员是一样,静态数据成员也有public 、private、protected 之分。在类外不能访问private、protected 的静态数据成员,可以访问public 的静态数据成员,访问时可以用“类名::”进行限制,或通过类对象访问。
private static初始化依然是在类外,但不能在主函数段。 代码如下:

#include <iostream>
using namespace std;
 
class Singleton
{
private:
 
	static Singleton* st;
	//static Singleton* st = NULL; //错误
 
	Singleton(){}
public:
 
	static Singleton* getInstance()
	{
		if (st == NULL)
		{
			st = new Singleton();
		}
 
		return st;
	}
 
	void show()
	{
		cout << st << endl;
	}
 
};
 
Singleton* Singleton::st = NULL; //正确,只能在类外初始化,如若不在此初始化会报连接错误
 
int main()
{
	//Singleton* Singleton::st = NULL; //错误
 
	Singleton* st = Singleton::getInstance();
	Singleton* st1 = Singleton::getInstance();
 
	if (st == st1)
	{
		cout << "两个对象是相同的实例。" << endl;
	}
 
	return 0;
}

静态成员函数

成员函数也可以声明为static的,方法是在类内成员函数声明或定义的前面冠以static关键字。如果在类体外定义静态成员函数,static 关键字只在类内声明时需要,类外定义时不需要,但要用“类名::”进行限制。
静态成员函数主要用来访问类的静态成员,不能直接访问类的非静态成员。
静态成员函数没有this指针。
与静态数据成员一样,在类外调用public 静态成员函数时,可以用“类名::”进行限制,或通过类对象访问。

常成员

[c++] 常成员函数_七秒钟笔记的博客-CSDN博客

指向成员的指针

https://www.runoob.com/w3cnote/cpp-func-pointer.html

组合类

其数据成员是其他类的。
当调用组合类的构造函数时,首先执行成员初始化列表中的操作,然后执行函数体内的操作。初始化列表中的初始化是按照数据成员在类中声明的顺序进行的,而与它们在列表中出现的顺序无关。

友元

总结

  1. 友元声明只能出现在类定义中;
  2. 友元只能通过它所在类的对象访问它所在类的成员;
  3. 友元关系不能被继承;
  4. 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明;
  5. 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A的友元,同样要看类中是否有相应的声明。

C++友元函数和友元类(C++ friend关键字)

第五章 运算符重载

C++ 重载运算符和重载函数 | 菜鸟教程
重载为类的友元函数时,一元运算符友元函数应带一个参数,二元运算符友元函数应带两个参数。这是因为友元函数没有this指针。
一般而言,应将一元运算符重载为成员函数,将二元运算符重载为友元函数。

类型转换运算符重载
例如以下代码

#include <iostream>
using namespace std;
class Complex
{
    double real, imag;
public:
    Complex(double r = 0, double i = 0) :real(r), imag(i) {};
    operator double() { return real; }  //重载强制类型转换运算符 double
};
int main()
{
    Complex c(1.2, 3.4);
    cout << (double)c << endl;  //输出 1.2
    double n = 2 + c;  //等价于 double n = 2 + c. operator double()
    cout << n;  //输出 3.2
}

C++ 流插入和流提取运算符的重载

C++函数对象详解(附带实例)

第六章 继承与派生

同名屏蔽现象
C/C++中同名覆盖原则和赋值兼容原则_c++同名覆盖原则_Game_Hacker的博客-CSDN博客
向上类型转换

image.png
从上一章我们知道,派生类的const 数据成员、引用型数据成员、类类型成员的初始化操作也在构造函数的初始化列表中进行。当调用派生类构造函数时,首先执行成员初始化列表中的操作,然后执行函数体内的操作。初始化列表中的执行顺序是:首先调用基类的构造函数(顺序与继承的顺序有关),然后才进行派生类的成员初始化(顺序与类内的声明顺序有关)。

使用虚基类解决多继承中的歧义问题

第七章 多态

使用虚函数时应注意以下几点。
■虚函数是成员函数,但不会是静态成员函数。
■若虚函数定义在类体外,则关键字virtual 只能出现在类内的函数声明前,在类外的函数定义前不需要再用该关键字。
■当使用作用域运算符“::”时,虚机制不再起作用。
作为重写的虚函数,派生类中的函数接口必须与基类中的完全相同,包括函数名、返回类型、参数列表、是否有const等。对函数返回类型可以稍微放松要求,如果基类中的虚函数返回类型是A* (或A&),那么派生类中重写的函数可以返回B*(或B&),其中B是A的public派生类。如果不满足这些要求,那么派生类中的同名函数就不是对基类虚函数的重写函数,而是重新定义的一个函数, 即使带有关键字virtual,也是如此。
■在派生类中重写虚函数时,如果原函数有默认形参值,就不要再定义新的形参值了。因为默认形参值是静态绑定的,只能来自基类的定义。
■在多层次继承中,如果派生类没有对基类的虚函数进行重写,将自动调用继承层次中最近的虚函数。
■只有虛函数才可能动态绑定。因此如果派生类要重写基类的行为,就应该将基类中的相应函数声明为virtual的。
总之,要想实现运行时的多态,必须满足三个基本条件:①public 继承;②虚函数;③通过指针(或引用)调用虚函数。通过对象操作不会发生动态绑定,因为编译时对象的类型是确定的,而指针(或引用)保存的只是地址,这意味着它可以是基类对象的地址,也可以是派生类对象的地址。

构造函数不能是虚函数,但析构函数可以是虚函数。若基类的析构函数是虚函数,则所有派生类的析构函数都自动是虚函数。

如何实现动态绑定

4.3C++动态绑定和虚函数表vtable

纯虚函数和抽象类

C++纯虚函数和抽象类详解

第八章 模板

函数模板

template <typename T1, typename T2>
类型 函数模板名(参数列表){……}

不是严格的函数,编译时实例化。
也可以重载。
专门化:

template <>
类型 函数模板名<具体数据类型>(参数列表){……}

类模板

template <typename T1, typename T2>
类定义

类模板中的成员函数都是函数模板。
类模板的成员函数如果定义在类外,必须是函数模板的形式。
类模板也可以专门化。
类模板可以使用友元,可以继承和派生,其基类和派生类可以是模板、也可以不是模板。

第九章 异常处理

C++ 异常处理 | 菜鸟教程
如果析构函数中抛出了异常,那么系统将变得非常危险。但是实际的软件开发很难保证析构函数中不会出现异常,如果可能出现异常,好的解决办法就是把异常处理完全封装在析构函数的内部,绝对不能让异常抛出析构函数之外。
对象的成员函数抛出异常时,该对象的析构函数仍然会得到执行,而且是在对象离开作用域时调用析构函数的。

第十章 C++标准库

【c/c++】刷算法题时常用的函数手册 持续更新--_c++ 函数手册_辉小歌的博客-CSDN博客
C++ 文件和流 | 菜鸟教程