1.1QT****介绍
1.1.1 QT****简介
Qt 是一个跨平台的应用程序和用户界面框架,用于开发++图形用户界面(++++GUI++ ++)++应用程序以及命令行工具。它最初由挪威的 Trolltech (奇趣科技)公司开发,现在由 Qt Company 维护,2020年12月8日发布QT6。Qt 使用 C++ 语言编写,支持多种编程语言通过绑定进行使用。
1.1.2 QT Creator 常用的快捷键使用基本介绍
2.1****命名空间
2.1.1****命名空间作用
创建自己的命名空间是 C++ 中组织代码的一种好方法,特别是在开发大型项目或库时。命名空间可以帮助你避免名称冲突,并且清晰地组织代码。
std 是 C++ 标准库的命名空间。它是一个定义在 C++ 标准库中的所有类、函数和变量的命名空间。
在 C++ 中,如果你想使用标准库中的任何类、函数或对象,你通常有两种选择:
- 使用std:: 前缀:这是最常见的方式,它明确指定了你正在使用的是位于 std 命名空间中的元
素。
std::cout << "Hello, world!" << std::endl;
- 使用using namespace std; :这允许你在不显式指定 std:: 的情况下使用 std 命名空间中的
所有元素。
using namespace std;
cout << "Hello, world!" << endl;
std****包含的内容
std 命名空间包含了许多类、函数和对象,例如:
输入输出库(如 std::cout , std::cin , std::endl )
容器类(如 std::vector , std::map , std::set )
字符串类( std::string )
异常类( std::exception 和相关子类)
算法(如 std::sort , std::find )
实用工具(如 std::pair , std::tuple )
其他许多功能
使用建议
对于小型代码或示例代码,使用 using namespace std; 通常是安全的。
对于大型项目或库,建议显式地使用 std:: 前缀,以避免潜在的名称冲突,并提高代码的可读性
和可维护性。
std 命名空间是 C++ 编程的基础部分,理解和正确使用它对于编写健壮和高效的 C++ 代码至关重要。
2.1.2****自定义命名空间
定义命名空间
假设我们要创建一个命名空间来包含与圆形相关的功能。我们可以命名这个命名空间为 cir :
在这个头文件中,我们定义了一个名为 cir 的命名空间,其中包含了计算圆的面积和周长的函数,以及圆周率常量 PI 。
使用命名空间
在另一个文件中,我们可以使用这个命名空间中定义的函数和常量:
在 main.cpp 中,我们首先包含了定义 Cir 命名空间的头文件。然后,我们可以使用 Cir:: 前缀来访 问该命名空间中的函数和常量。
通过使用自定义命名空间,你可以有效地组织你的代码,并减少不同库之间的名称冲突。这在大型项目和团队协作中尤其重要。
输出结果
2.2从C****语言快速入门
2.2.1****输入输出
C++ 中的输入和输出(I/O)主要是通过标准库中的输入输出流来实现的。最常用的是 iostream 库,它提供了用于输入和输出的基本流类,包括 cin 、 cout 、 cerr 和 clog 。
标准输出流**(**cout )
cout 代表标准输出流,通常用于向屏幕输出数据。
使用操作符 << (插入操作符)向 cout 发送数据。
例如, std::cout << "Hello, world!" << std::endl; 会在屏幕上打印 "Hello, world!" 并换行。
标准输入流**(**cin )
cin 代表标准输入流,用于从键盘接收数据。
使用操作符 >> (提取操作符)从 cin 提取数据。
例如, int x; std::cin >> x; 会从用户那里读取一个整数并存储在变量 x 中。
标准错误流**(**cerr **)和标准日志流(**clog )
cerr 用于输出错误消息。与 cout 不同, cerr 不是缓冲的,这意味着它会立即输出。
clog 类似于 cerr ,但它是缓冲的。它通常用于记录错误和日志信息。
2.2.2****基本变量类型
C++ 基本数据类型整理成表格。以下是一个表格,展示了不同的基本数据类型及其一般用途和大小范围:
和C语言类似。
2.2.3****内联函数
内联函数(Inline Function)是C++中一种特殊的函数,其定义直接在每个调用点展开。这意味着编译器会尝试将函数调用替换为函数本身的代码,这样可以减少函数调用的开销,尤其是在小型函数中。
特点
-
减少函数调用开销:内联函数通常用于++优化小型、频繁调用++ 的函数,因为它++避免了函数调用的常规开销(如参数传递、栈操作等)++。
-
编译器决策:即使函数被声明为内联,编译器也可能决定不进行内联,特别是对于复杂或递归函 数。
-
适用于小型函数:通常只有++简单的、执行时间短的函数适合做内联++。
-
定义在每个使用点:内联函数的定义(而非仅仅是声明)必须对每个使用它的文件都可见,通常意味着将内联函数定义在头文件中。
使用方法
通过在函数声明前添加关键字 inline 来指示编译器该函数适合内联:
inline int max(int x, int y)
{
return x > y ? x : y;
}
示例
#include <iostream>
inline int add(int a, int b)
{
return a + b;
}
int main()
{
int result = add(5, 3); // 编译器可能会将此替换为:int result = 5 + 3;
std::cout << "Result: " << result << std::endl;
return 0;
}
在这个示例中,函数 add 被定义为内联函数。当它被调用时,编译器可能会将函数调用替换为函数体内的代码。
注意事项
过度使用的风险:不应滥用内联函数,因为这可能会增加最终程序的大小(代码膨胀)。对于大型
函数或递归函数,内联可能导致性能下降。
编译器的决定:最终是否将函数内联是由编译器决定的,即使函数被标记为 inline 。
适用场景:最适合内联的是小型函数和在性能要求高的代码中频繁调用的函数。
内联函数是一种用于优化程序性能的工具,但需要合理使用,以确保代码的可维护性和性能的平衡。
2.2.4 Lambda****表达式
Lambda 表达式是 C++11 引入的一种匿名函数的方式,它允许你在需要函数的地方内联地定义函数,而无需单独命名函数
Lambda 表达式的基本语法如下:
[capture clause](parameters) -> return_type
{
// 函数体
// 可以使用捕获列表中的变量
return expression; // 可选的返回语句
}
Lambda 表达式由以下部分组成:
捕获列表(Capture clause):用于捕获外部变量,在 Lambda 表达式中可以访问这些变量。捕
获列表可以为空,也可以包含变量列表 [var1, var2, ...] 。
参数列表(Parameters):与普通函数的参数列表类似,可以为空或包含参数列表 (param1,
param2, ...) 。
返回类型(Return type):Lambda 表达式可以自动推断返回类型auto,也可以显式指定返回类
型 -> return_type 。如果函数体只有一条返回语句,可以省略返回类型。
函数体(Body):Lambda 表达式的函数体,包含需要执行的代码。
Lambda 表达式最简单的案例是在需要一个小型函数或临时函数时直接使用它。以下是一个非常简单的例子,其中使用 Lambda 表达式来定义一个加法操作,并立即使用它来计算两个数的和。
#include <iostream>
int main()
{
// 定义一个简单的 Lambda 表达式进行加法
auto add = [ ](int a, int b)
{
return a + b;
};
// 使用 Lambda 表达式计算两个数的和
int sum = add(10, 20);
std::cout << "Sum is: " << sum << std::endl;
return 0;
}
在这个例子中:
我们定义了一个名为 add 的 Lambda 表达式,它接受两个整数参数,并返回它们的和。然后,我们使用这个 Lambda 表达式来计算两个数字(10 和 20)的和,并将结果存储在变量 sum中。最后,我们打印出这个和。
这个例子展示了 Lambda 表达式的基本用法:作为一种简洁而快速的方式来定义小型函数。
我们可以写一个例子,其中使用一个函数来找出两个数中的较大数,这个函数将接受一个 lambda 函数作为回调来比较这两个数。Lambda 函数将直接在函数调用时定义,完全是匿名的。
先回忆以下回调函数
#include <iostream>
bool myCompare(int a, int b)
{
return a > b;
}
int getMax( int a, int b, bool (*compare)(int, int) )
{
if (compare(a, b))
{
return a;
} else
{
return b;
}
}
int main()
{
int x = 10;
int y = 20;
// 回调函数
int max = getMax(x, y, myCompare);
std::cout << "The larger number is: " << max << std::endl;
return 0;
}
示例:使用匿名Lambda函数来返回两个数中的较大数
#include <iostream>
// 函数,接受两个整数和一个比较的 lambda 函数
int getMax(int a, int b, bool(*compare)(int, int))
{
if (compare(a, b))
{
return a;
} else
{
return b;
}
}int main()
{
int x = 10;
int y = 20;
// 直接在函数调用中定义匿名 lambda 函数
int max = getMax(x, y, [ ](int a, int b) -> bool
{
return a > b;
});
std::cout << "The larger number is: " << max << std::endl;
return 0;
}
在这个例子中:
getMax 函数接受两个整数 a 和 b ,以及一个比较函数 compare 。这个比较函数是一个指向函数 的指针,它接受两个整数并返回一个布尔值。在 main 函数中,我们调用 getMax ,并直接在调用点定义了一个匿名的 lambda 函数。这个lambda 函数接受两个整数并返回一个表示第一个整数是否大于第二个整数的布尔值。这个 lambda 函数在 getMax 中被用作比较两个数的逻辑。根据 lambda 函数的返回值, getMax返回较大的数。
这个例子展示了如何直接在函数调用中使用匿名 lambda 函数,使代码更加简洁和直接。这种方法在需要临时函数逻辑的场合非常有用,尤其是在比较、条件检查或小型回调中。
在 Lambda 表达式中,参数捕获是指 Lambda 表达式从其定义的上下文中捕获变量的能力。这使得Lambda 可以使用并操作在其外部定义的变量。捕获可以按值(拷贝)或按引用进行。
让我们通过一个简单的示例来展示带参数捕获的 Lambda 表达式。
输出
在这个例子中:
第一个 Lambda 表达式 sum 按值捕获了 x 和 y (即它们的副本)。这意味着 sum 内的 x 和 y
是在 Lambda 定义时的值的拷贝。
第二个 Lambda 表达式 mul 使用 [=] 捕获列表,这表示它按值捕获所有外部变量。
第三个 Lambda 表达式 modify_mul 使用 [&] 捕获列表,这表示它按引用捕获所有外部变量。因此,它可以修改 x 和 y 的原始值。
这个示例展示了如何使用不同类型的捕获列表(按值和按引用)来控制 Lambda 表达式对外部变量的访问和修改。按值捕获是安全的,但不允许修改原始变量,而按引用捕获允许修改原始变量,但需要注意引用的有效性和生命周期问题。
以下是一个表格,概述了 Lambda 函数和内联函数在 C++ 中的相似之处和区别:
请注意,虽然****Lambda 函数和内联函数在某些方面有相似之处,如它们都可以被编译器优化以减少调用开销 ,但它们在设计和用途上有明显的不同。Lambda****函数的核心优势在于它们的匿名性和对外部变量的捕获能力,而内联函数则主要关注于提高小型函数的性能。
2.3类
2.3.1****类的初探
C++ 中的类(class)是一种编程结构,用于创建对象。这些对象可以拥有属性(即数据成员)和行为(即成员函数或方法)。类的概念是面向对象编程的核心之一,其主要目的是将数据和与数据相关的操****作封装在一起。例如,如果你有一个"汽车"类,它可能包含颜色、品牌、型号等属性(数据成员),以及启动、停止、加速等行为(成员函数)。每当你基于这个类创建一个对象时,你就有了一个具体的汽车,具有这些属性和行为。
C++ 类的基本结构通常包含:
-
数据成员(Attributes):定义类的属性。这些是类内部的变量,用于存储对象的状态。
-
成员函数(Methods):定义类的行为。这些是可以操作对象的数据成员的函数。
-
构造函数和析构函数:特殊的成员函数。构造函数在创建对象时自动调用,用于初始化对象。析构函数在对象销毁时调用,用于执行清理操作。
-
访问修饰符:如 public , private , protected ,用于控制对类成员的访问权限。例如, public成员可以在类的外部访问,而 private 成员只能在类内部访问。
-
继承:允许一个类继承另一个类的特性。这是代码重用和多态性的关键。
通过这些特性,C++ 类提供了一种强大的方式来组织和处理数据,使得代码更加模块化、易于理解和维护。
2.3.2****结构体引入类
2.3.2.1****回忆结构体
如果用C语言实现上面描述的汽车类,我们实现如下代码
输出
**2.3.2.2新建C++**工程来使用结构体
在C++中,字符串用string来表示,发现有个string赋值给char 的警告,所以修改所有char *为 string类型
main . cpp : 33 : 17 : warning : ISO C ++ 11 does not allow conversion from string literal
to 'char *'
修改后,发现printf的%s控制位,不能用于string的输出,所有有string构建了即将要输出的字符串
C++中,通过std::tostring()函数,将整型数转化成字符串
在printCarInfo中使用cout输出汽车信息
发现在C++工程中,使用malloc在堆申请结构体空间有问题,所以直接在此引入类的概念,把struct改成class
引入新问题,class的成员数据和成员函数在不指定权限的情况下,默认private权限,类的对象无法进行直接访问
main.cpp:33:9: error: 'color' is a private member of 'Car'
main.cpp:5:11: note: implicitly declared private here
添加public属性
把main函数中的原本结构体变量改成了类的实例化,如果变量类型是指针,把原来的malloc改成
new一个对象
最后解决了所有问题
但是!我们还没有真正体验到面向对象的封装特性,仅仅感受到权限上的区别
2.3.2.3****真正的成员函数
上一节的案例中, void (*printCarInfo)(string color,string brand,string type, int year); 到底是变量函数还是成员函数呢?
答:是一个指针变量,是保存某个函数地址的变量,所以它不是成员函数,是成员数据
真正的成员函数遵守封装特性,在函数体内部访问成员数据的时候,不需要参数传递
在 C++ 中,双冒号 :: 称为 "作用域解析运算符"(Scope Resolution Operator)。它用于指定一
个成员(如函数或变量)属于特定的类或命名空间。例如,在类的外部定义成员函数时, :: 用于
指明该函数属于哪个类。
2.3.4 QT****中经常出现的用法
在 C++中,一个类包含另一个类的对象称为组合(Composition)。这是一种常见的设计模式,用
于表示一个类是由另一个类的对象组成的。这种关系通常表示一种"拥有"("has-a")的关系。
普通变量访问成员变量或者成员函数,使用 " . " 运算符
指针变量访问成员变量或者成员函数,使用" -> "运算符,像C语言的结构体用法
输出
2.4****权限初识
2.4.1****基本介绍
C++中的访问权限主要分为三种: public 、 private 和 protected 。这些权限决定了类成员(包括数据成员和成员函数)的可访问性。以下是一个总结表格,说明了在不同情况下这些权限如何应用:
使用权限(如 public 、 private 和 protected )在C++中是一种关键的封装手段,它们旨在控制对类成员的访问。下面是一个表格,总结了使用权限的主要好处和潜在缺点:
2.4.2****目前能概况的结论
public 权限相当于我们学习C语言结构体一样,不考虑访问权限的存在,但是要注意,类中不写权
限,默认是私有权限
protected 留到继承讲解的时候再提
private 私有权限,通过一下案例向各位表达一下作用的意思,但需要未来实战中慢慢体会。
这个例子将阐述在类设计中使用 private 成员的必要性。我们将创建一个简单的 BankAccount 类,展示如何使用 private 来保护账户的余额,确保它只能通过指定的方法进行修改。
所以,我们可以脑部一个场景:
银行的账户是一个模板,是一个类,有存款人信息和账户额度,而具体的存款人视为一个对象,一个对象不能私自修改账户额度,需要通过一个操作流程,比如去ATM或者柜台进行操作才能修改到账户额度,所以,存款人信息和账户额度设计成私有权限,通过公有的操作流程,也就是公有函数去操作私有变量。
基于这个场景,我们编程实现代码
输出
在这个示例中, balance 是一个 private 成员变量,它不能被类的外部直接访问。这保证了账户余额只能通过类提供的方法(如 putinMoney , getfromMoney, 和 getBalance )来修改和查询,从而防止了不合适的修改,比如直接设置余额为负数或任意值。这样的设计保证了类的封装性和数据的完整性。
2.4.3****提问和回答
问:为什么新手学习C++感受不到访问权限的必要性呢?
答:新手学习C++时可能不会立即感受到访问权限(如 public 、 private 、 protected )的必要性,主要有以下几个原因:
-
简单的例子和练习:初学者通常从简单的例子和练习开始,这些例子可能不需要复杂的封装或继承结构。在这种情况下,访问权限的作用可能不太明显。
-
封装的概念需要时间去理解:封装是面向对象编程中的一个核心概念,但对于初学者来说,理解封装的价值需要一定的时间和实践。在初期,更多的关注点可能放在基本语法和程序结构上。
-
缺乏大型项目经验:在小型项目或单文件程序中,访问权限的重要性可能不如在大型、多人协作的项目中那么显著。在复杂的软件开发中,适当的访问控制对于代码的维护性和可读性至关重要。
-
直接操作感觉更简单:对于初学者来说,直接访问和修改类的所有成员可能看起来更简单直接。他们可能还没有遇到由于不恰当访问控制导致的维护和调试问题。
-
抽象和设计模式的理解:理解何时以及如何使用访问权限通常涉及到对软件设计模式和抽象的深入理解。这些通常是随着经验积累和更深入的学习而逐渐掌握的。
随着经验的增长,学习者开始处理更复杂的项目,他们将开始意识到恰当的访问控制的重要性,特别是在保持代码的可维护性、可读性以及在团队环境中的协作方面。因此,对于教育者和学习者来说,强调并实践这些概念是很重要的,以便在编程技能成熟时能够有效地运用它们。
2.5****引用
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。
一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
思维发散:
在C语言中,一个数据对应一个内存,通过由一个变量名来访问这个内存空间的数据,叫做直接访问,相对直接访问,有个间接访问的说法,叫做指针。
而引用相当于又给这个内存中的数据提供了一个新的变量名,
这个变量名功能比传统变量名更特殊,是直达地址的,后续代码验证!
2.5.1****和指针的区别
引用很容易与指针混淆,它们之间有三个主要的不同:
1.不存在空引用。引用必须连接到一块合法的内存。
2.一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
3.引用必须在创建时被初始化。指针可以在任何时间被初始化。
官方没有明确说明,但是引用确实不是传统意义上的独立变量,它不能"变"嘛
试想变量名称是变量附属在内存位置中的标签,可以把引用当成是变量附属在内存位置中的第二
个标签。因此,可以通过原始变量名称或引用来访问变量的内容。例如:
int i = 17 ; int* p = & i ; * p = 20 ;
我们可以为 i 声明引用变量,如下所示:
int& r = i;
double& s = d;
在这些声明中,& 读作引用。
因此,第一个声明可以读作 "r 是一个初始化为 i 的整型引用",第二个声明可以读作 "s 是一个初始化为 d 的 double 型引用"。
下面的实例使用了 int 和 double 引用:
输出
2.5.2****把引用作为参数
我们已经讨论了如何使用指针来实现引用调用函数。下面的实例使用了引用来实现引用调用函数。
输出
2.5.3****把引用作为返回值
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。
当函数返回一个引用时,则返回一个指向返回值的隐式指针。
这样,函数就可以放在赋值语句的左边。
例如,请看下面这个简单的程序:
输出
2.6****重载
2.6.1****函数重载
在同一个作用域内,可以声明几个++功能类似的同名函数++ ,这些同名函数的形式参数(++指参数的个数、类型或者顺序++)必须不同。不能仅通过返回类型的不同来重载函数。
下面的实例中,同名函数 **print()**被用于输出不同的数据类型:
输出
2.7****构造函数
2.7.1****什么是构造函数
类的构造函数是类的一种++特殊的成员函数++,它会在每次创建类的新对象时执行。
构造,那构造的是什么呢?
构造成员变量的初始化值,内存空间等
++构造函数的名称与类的名称是完全相同的++,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
2.7.2****带参数构造函数
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值
下面的实例有助于更好地理解构造函数的概念:
输出
2.7.3****使用初始化列表
在C++中,使用初始化列表来初始化类的字段是一种高效的初始化方式,尤其在构造函数中。**初始化列表直接在对象的构造过程中初始化成员变量,而不是先创建成员变量后再赋值。**这对于提高性能尤其重要,特别是在涉及到复杂对象或引用和常量成员的情况下。
初始化列表紧跟在构造函数参数列表后面,以冒号( : **)开始,后跟一个或多个初始化表达式,每个表达式通常用逗号分隔。**下面是使用初始化列表初始化字段的例子:
输出
初始化列表的优点包括:
-
效率:对于非基本类型的对象,使用初始化列表比在构造函数体内赋值更高效,因为它避免了先默认构造然后再赋值的额外开销。
-
必要性:对于引用类型和常量类型的成员变量,必须使用初始化列表,因为这些类型的成员变量在构造函数体内不能被赋值。
-
顺序:成员变量的初始化顺序是按照它们在类中声明的顺序,而不是初始化列表中的顺序。
使用初始化列表是C++中推荐的初始化类成员变量的方式,因为它提供了更好的性能和灵活性。
2.7.4 this****关键字
在 C++ 中, this 关键字是一个指向调用对象的指针。它在成员函数内部使用,用于引用调用该函数的对象。使用 this****可以明确指出成员函数正在操作的是哪个对象的数据成员。
示例代码
下面的代码展示了如何使用 this 关键字:
输出
在这个例子中, Car 类的构造函数使用 this 指针来区分成员变量和构造函数参数。同样, setYear 成员函数使用 this 指针来返回调用该函数的对象的引用,这允许链式调用,如 myCar.setYear(2021).display(); 。在 main 函数中创建了 Car 类型的对象,并展示了如何使用这 些成员函数。
2.7.5 new****关键字
在C++中, new 关键字用于动态分配内存。它是C++中处理动态内存分配的主要工具之一,允许在程序运行时根据需要分配内存。
基本用法
分配单个对象:使用 new 可以在堆上动态分配一个对象。例如, new int 会分配一个 int 类型的空
间,并返回一个指向该空间的指针。
int* ptr = new int; //C语言中,int *p = (int *)malloc(sizeof(int));
分配对象数组: new 也可以用来分配一个对象数组。例如, new int[10] 会分配一个包含10个整数的数组。
int* arr = new int [ 10 ]; //C 语言中, int *arr = (int *)malloc(sizeof(int)*10);
初始化:可以在 new 表达式中使用初始化。对于单个对象,可以使用构造函数的参数:
MyClass * obj = new MyClass ( arg1 , arg2 );
与delete 配对使用
使用 new 分配的内存必须显式地通过 delete (对于单个对象)或 delete[] (对于数组)来释放,以避免内存泄露:
释放单个对象:
delete ptr; // 释放 ptr 指向的对象
释放数组:
delete[] arr; // 释放 arr 指向的数组
注意事项
异常安全:如果 new 分配内存失败,它会抛出 std::bad_alloc 异常。
内存泄露:忘记释放使用 new 分配的内存会导致内存泄露。
匹配使用delete 和delete[ ] :为避免未定义行为,使用 new 分配的单个对象应该使用
delete 释放,使用 new[ ] 分配的数组应该使用 delete[ ] 释放。
示例代码
class MyClass
{
public :
MyClass ()
{
std::cout << "Object created" << std::endl ;
}
};
int main ()
{
// 分配单个对象
MyClass * myObject = new MyClass ();
// 分配对象数组
int* myArray = new int [ 5 ]{ 1 , 2 , 3 , 4 , 5 };
// 使用对象和数组 ...
// 释放内存
delete myObject ;
delete [ ] myArray ;
return 0 ;
}
在这个例子中, new 被用来分配一个 MyClass 类型的对象和一个整数数组,然后使用 delete 和
delete[] 来释放内存。每个 new 都对应一个 delete ,保证了动态分配的内存被适当管理。
2.8****析构函数
2.8.1****什么是析构函数
析构函数是C++中的一个特殊的成员函数,它在对象生命周期结束时被自动调用,用于执行对象销毁前的清理工作。析构函数特别重要,尤其是在涉及动态分配的资源(如内存、文件句柄、网络连接等)的情况下。
基本特性
-
名称:析构函数的名称由波浪号( ~ )后跟类名构成,如 ~MyClass() 。
-
无返回值和参数:++析构函数不接受任何参数,也不返回任何值。++
-
自动调用:当对象的生命周期结束时(例如,一个局部对象的作用域结束,或者使用 delete 删除一个动态分配的对象),析构函数会被自动调用。
-
不可重载:++每个类只能有一个析构函数。++
-
继承和多态:如果一个类是多态基类,其析构函数应该是虚的。
示例
假设我们有一个类 MyClass ,它包含了动态分配的内存或其他资源:
输出
在这个示例中, MyClass 的构造函数分配了一块内存,而析构函数释放了这块内存。当 obj 的生命周期结束时(即离开了它的作用域), MyClass 的析构函数被自动调用,负责清理资源,防止内存泄露。
重要性
析构函数在管理资源方面非常重要。没有正确实现析构函数,可能导致资源泄露或其他问题。在基于RAII(资源获取即初始化)原则的C++编程实践中,确保资源在对象析构时被适当释放是非常关键的。当使用智能指针和其他自动资源管理技术时,可以减少显式编写析构函数的需要,但了解析构函数的工作原理仍然很重要。
以下是关于 C++ 中析构函数需要了解的十个要点的表格:
2.9****静态成员
2.9.1****静态成员的定义
静态成员在C++类中是一个重要的概念,它包括静态成员变量和静态成员函数。静态成员的特点和存在的
意义如下:
静态成员变量
-
定义:静态成员变量是类的所有对象共享的变量。与普通成员变量相比,无论创建了多少个类的实例,静态成员变量只有一份拷贝。
-
初始化:静态成员变量需要在类外进行初始化,通常在类的实现文件中。
-
访问:静态成员变量可以通过类名直接访问,不需要创建类的对象。也可以通过类的对象访问。
-
用途:常用于存储类级别的信息(例如,计数类的实例数量)或全局数据需要被类的所有实例共 享。
静态成员函数
-
定义:静态成员函数是可以不依赖于类的实例而被调用的函数。它不能访问类的非静态成员变量和非静态成员函数。
-
访问:类似于静态成员变量,静态成员函数可以通过类名直接调用,也可以通过类的实例调用。
-
用途:常用于实现与具体对象无关的功能,或访问静态成员变量。
示例代码
存在的意义
共享数据:允许对象之间共享数据,而不需要每个对象都有一份拷贝。
节省内存:对于频繁使用的类,使用静态成员可以节省内存。
独立于对象的功能:静态成员函数提供了一种在不创建对象的情况下执行操作的方法,这对于实现
工具函数或管理类级别状态很有用。
2.9.2****静态成员变量的作用
静态成员变量在C++中的一个典型应用是用于跟踪类的实例数量。这个案例体现了静态成员变量的特性:
它们在类的所有实例之间共享,因此适合于存储所有实例共有的信息。
下面是一个示例,展示了如何使用静态成员变量来计数一个类的实例数量:
在这个例子中:
Myclass 类有一个静态成员变量 staticNumofInstance ,用来跟踪该类的实例数量。
每当创建 Myclass 的新实例时,构造函数会增加 staticNumofInstance 。
每当一个 Myclass 实例被销毁时,析构函数会减少 staticNumofInstance 。
通过静态成员函数 getNunofInstance 可以随时获取当前的实例数量。
静态成员变量 staticNumofInstance 在类外初始化为0。
这个案例展示了静态成员变量如何在类的所有实例之间共享,并为所有实例提供了一个共同的状态(在这个例子中是实例的数量)。这种技术在需要跟踪对象数量或实现某种形式的资源管理时特别有用。
2.10****继承
2.10.1****继承基本概念
继承是面向对象编程(OOP)中的一个核心概念,特别是在C++中。它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。继承的主要目的是实现代码重用,以及建立一种类型之间的层次关系。
特点
-
代码重用:子类继承了父类的属性和方法,减少了代码的重复编写。
-
扩展性:子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
-
多态性:通过继承和虚函数,C++支持多态,允许在运行时决定调用哪个函数。
基本用法
在C++中,继承可以是公有(public)、保护(protected)或私有(private)的,这决定了基类成员在派生类中的访问权限。
输出
在这个例子中, Bickle 类和Roadster类公有地继承自 Vehicle 类,这意味着所有 Vehicle 类的公有成员在Bickle 类和Roadster类中也是公有的。
2.10.2****权限对继承的影响
在C++中,访问控制符对继承的影响可以通过下表来清晰地展示。这个表格展示了不同类型的继承
( public 、 protected 、 private )如何影响基类的不同类型成员( public 、 protected 、
private )在派生类中的访问级别。
解释:
public 继承:基类的 public 成员在派生类中仍然是 public 的, protected 成员仍然是
protected 的。基类的 private 成员在派生类中不可访问。
protected 继承:基类的 public 和 protected 成员在派生类中都变成 protected 的。基类
的 private 成员在派生类中不可访问。
private 继承:基类的 public 和 protected 成员在派生类中都变成 private 的。基类的
private 成员在派生类中不可访问。
这个表格提供了一个快速参考,帮助理解在不同类型的继承中基类成员的访问级别是如何变化的。记住,无论继承类型如何,基类的 private****成员始终不可直接在派生类中访问。
2.10.3****基类构造函数
在C++中,派生类可以通过其构造函数的初始化列表来调用基类的构造函数 。这是在构造派生类对象时初始化基类部分的标准做法。
当创建派生类的对象时,基类的构造函数总是在派生类的构造函数之前被调用。如果没有明确指定,将调用基类的默认构造函数。如果基类没有默认构造函数,或者你需要调用一个特定的基类构造函数,就****需要在派生类构造函数的初始化列表中明确指定。
输出
2.10.4****虚函数
在C++中, virtual 和 override 关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。正确地理解和使用这两个关键字对于编写可维护和易于理解的面向对象代码至关重要。
virtual****关键字
-
使用场景:在基类中声明虚函数。
-
目的:允许派生类重写该函数,实现多态。
-
行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本。
override****关键字
-
使用场景:在派生类中重写虚函数。
-
目的:明确指示函数意图重写基类的虚函数。
-
行为:确保派生类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报
错。
输出:自行车跑起来
注意点
只在派生类中使用****override: override 应仅用于派生类中重写基类的虚函数。
虚析构函数:如果类中有虚函数,通常应该将析构函数也声明为虚的。
默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用 virtual
关键字才会成为虚函数。
继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有派生类中自动成为虚函数,无论是否
使用 virtual 关键字。
正确使用 virtual 和 override 关键字有助于清晰地表达程序员的意图,并利用编译器检查来避免常
见的错误,如签名不匹配导致的非预期的函数重写。
2.10.5****多重继承
在C++中,多重继承是一种允许一个类同时继承多个基类的特性。这意味着派生类可以继承多个基类的属性和方法。多重继承增加了语言的灵活性,但同时也引入了额外的复杂性,特别是当多个基类具有相同的成员时。
基本概念
在多重继承中,派生类继承了所有基类的特性。这包括成员变量和成员函数。如果不同的基类有相同名称的成员,则必须明确指出所引用的是哪个基类的成员。
示例
假设有两个基类 ClassA 和 ClassB ,以及一个同时从这两个类继承的派生类 Derived :
class ClassA
{
public:
void displayA()
{
std::cout << "Displaying ClassA" << std::endl;}
};
class ClassB
{
public:
void displayB()
{
std::cout << "Displaying ClassB" << std::endl;
}
};
class Derived : public ClassA, public ClassB
{
public:
void display()
{
displayA(); // 调用 ClassA 的 displayA
displayB(); // 调用 ClassB 的 displayB
}
};
int main()
{
Derived obj;
obj.displayA(); // 调用 ClassA 的 displayA
obj.displayB(); // 调用 ClassB 的 displayB
obj.display(); // 调用 Derived 的 display
return 0;
}
在这个示例中, Derived 类同时继承了 ClassA 和 ClassB 。因此,它可以使用这两个类中定义的方法。
在这个实例中,如果不同的基类有相同名称的成员,则必须明确指出所引用的是哪个基类的成员。
注意事项
菱形继承问题:如果两个基类继承自同一个更高层的基类,这可能导致派生类中存在两份基类的副
本,称为菱形继承(或钻石继承)问题。这可以通过虚继承来解决。
复杂性:多重继承可能会使类的结构变得复杂,尤其是当继承层次较深或类中有多个基类时。
设计考虑:虽然多重继承提供了很大的灵活性,但过度使用可能导致代码难以理解和维护。在一些
情况下,使用组合或接口(纯虚类)可能是更好的设计选择。
多重继承是C++的一个强大特性,但应谨慎使用。合理地应用多重继承可以使代码更加灵活和强大,但不当的使用可能导致设计上的问题和维护困难。
2.10.6****虚继承
虚继承是C++中一种特殊的继承方式,主要用来解决多重继承中的菱形继承问题。在菱形继承结构中,一个类继承自两个具有共同基类的类时,会导致共同基类的成员在派生类中存在两份拷贝,这不仅会导致资源浪费,还可能引起数据不一致的问题。虚继承通过确保共同基类的单一实例存在于继承层次中,来解决这一问题。
菱形继承问题示例
考虑以下的类结构:
class Base
{
public:
int data;
};class Derived1 : public Base
{
// 继承自 Base
};
class Derived2 : public Base
{
// 继承自 Base
};
class FinalDerived : public Derived1, public Derived2
{
// 继承自 Derived1 和 Derived2
};
在这个例子中, FinalDerived 类通过 Derived1 和 Derived2 间接地继承自 Base 类两次。因此,
它包含了两份 Base 的成员拷贝。
使用虚继承解决菱形继承问题
要解决这个问题,应使用虚继承:
class Base
{
public:
int data;
};
class Derived1 : virtual public Base
{
// 虚继承 Base
};
class Derived2 : virtual public Base
{
// 虚继承 Base
};
class FinalDerived : public Derived1, public Derived2
{
// 继承自 Derived1 和 Derived2
};
通过将 Derived1 和 Derived2 对 Base 的继承声明为虚继承( virtual public Base ),
FinalDerived 类中只会有一份 Base 类的成员。无论通过 Derived1 还是 Derived2 的路径,访问
的都是同一个 Base 类的成员。
特点和注意事项
初始化虚基类:在使用虚继承时,虚基类(如上例中的 Base 类)只能由最派生的类(如
FinalDerived )初始化。
内存布局:虚继承可能会改变类的内存布局,通常会增加额外的开销,比如虚基类指针。
设计考虑:虚继承应谨慎使用,因为它增加了复杂性。在实际应用中,如果可以通过其他设计(如
组合或接口)避免菱形继承,那通常是更好的选择。
虚继承是C++语言中处理复杂继承关系的一种重要机制,但它也带来了一定的复杂性和性能考虑。正确地使用虚继承可以帮助你建立清晰、有效的类层次结构。
2.11****多态
多态的基本概念(polymorphic)
想象一下,你有一个遥控器(这就像是一个基类的指针),这个遥控器可以控制不同的电子设备(这些设备就像是派生类)。无论是电视、音响还是灯光,遥控器上的"开/关"按钮(这个按钮就像是一个虚函数)都能控制它们,但具体的操作(打开电视、播放音乐、开灯)则取决于你指向的设备。
2.11.1****如何实现多态
- 使用虚函数(Virtual Function):
我们在基类中定义一个虚函数,这个函数可以在任何派生类中被"重写"或者说"定制"。
使用关键字 virtual 来声明。
- 创建派生类并重写虚函数:
在派生类中,我们提供该虚函数的具体实现。这就像是告诉遥控器,"当你控制我的这个设备
时,这个按钮应该这样工作"。
- 通过基类的引用或指针调用虚函数:
当我们使用基类类型的指针或引用来调用虚函数时,实际调用的是对象的实际类型(派生类)
中的函数版本。
在这个例子中,不同的对象( TvRemoteCon 和 TvRemoteCon )以它们自己的方式"开",尽管调用的是相同的函数 openUtils 。这就是多态的魅力------相同的接口,不同的行为。
为什么使用多态
灵活性:允许我们编写可以处理不确定类型的对象的代码。
可扩展性:我们可以添加新的派生类而不必修改使用基类引用或指针的代码。
接口与实现分离:我们可以设计一个稳定的接口,而将具体的实现留给派生类去处理。
2.11.2****抽象类
抽象类的基本概念
想象一下,你有一个"交通工具"的概念。这个概念告诉你所有交通工具都应该能做什么,比如移动
(move),但它并不具体说明怎么移动。对于不同的交通工具,比如汽车和自行车,它们的移动方式是不同的。在这个意义上,"交通工具"是一个抽象的概念,因为它本身并不能直接被使用。你需要一个具体的交通工具,比如"汽车"或"自行车",它们根据"交通工具"的概念具体实现了移动的功能。
在 C++ 中,抽象类就像是这样的一个抽象概念。它定义了一组方法(比如移动),但这些方法可能没有具体的实现。这意味着,抽象类定义了派生类应该具有的功能,但不完全实现这些功能。
抽象类的特点
- 包含至少一个纯虚函数:
抽象类至少有一个纯虚函数。这是一种特殊的虚函数,在抽象类中没有具体实现,而是留给派
生类去实现。
++纯虚函数的声明方式是在函数声明的末尾加上 = 0 。++
- 不能直接实例化:
由于抽象类不完整,所以不能直接创建它的对象。就像你不能直接使用"交通工具"的概念去任
何地方,你需要一个具体的交通工具。
- 用于提供基础结构:
抽象类的主要目的是为派生类提供一个共同的基础结构,确保所有派生类都有一致的接口和行
为。
#include <iostream>
using namespace std;
class Teacher
{
public:
string name;
string shool;
string major;
virtual void goInClass() = 0;
virtual void startTeaching() = 0;
virtual void afterTeaching() = 0;};
class EnglishTeacher : public Teacher
{
public:
void goInClass() override
{
cout << "英语老师开始进入教室" << endl;
}
void startTeaching() override
{
cout << "英语老师开始教学" << endl;
}
void afterTeaching() override
{
};
};
class ProTeacher : public Teacher
{
public:
void goInClass() override
{
cout << "编程老师开始进入教室" << endl;
}
void startTeaching() override
{
cout << "编程老师开始撸代码了,拒绝读PPT" << endl;
}
void afterTeaching() override
{
cout << "编程老师下课后手把手教x学员写代码" << endl;
};
};
int main()
{
// Teacher t;//抽象类,不支持被实例化
EnglishTeacher e;
e.goInClass();
ProTeacher t;
t.startTeaching();
t.afterTeaching();
//抽象类,多态
Teacher *teacher = new ProTeacher;
teacher->startTeaching();
return 0;
}