万能头

1
#include<bits/stdc++.h>

C++基础知识

1. 常量

在C++中,常量是使用关键字const声明的,意味着一旦被赋值后,它的值就不能被改变。常量提供了一种保护变量不被无意或有意修改的方式,有助于提高程序的可读性和维护性。以下是一些关于C++常量的关键点:

  1. 基本常量
  • 使用const关键字创建常量。
  • 必须在声明时初始化。
1
const int MAX_USERS = 100;


  1. 枚举常量
  • 枚举(Enumeration)是一种用户定义的类型,它包含一组命名的整型常量。
  • 使用enum关键字声明。
1
enum Color { RED, GREEN, BLUE };


  1. 宏常量
  • 使用预处理器指令#define来定义。
  • 宏在编译之前被处理,替换文本中所有的宏名称。
1
#define PI 3.14159


  1. 字面量常量
  • 直接出现在代码中的值,如整数10,浮点数3.14,字符'A',字符串"Hello"
为什么字面量常量是常量?

字面量常量是常量,因为它们表示固定的值,不能被修改。在C++(以及其他编程语言)中,当你在代码中直接使用一个值,如数字42、字符'A'或字符串"Hello"时,这些值就是字面量。字面量的特点是它们在源代码中直接出现,且它们的值在编写代码的时候就已经确定,而不是在程序运行时计算得到的。

为什么是常量?

  • 不变性:字面量表示的值是不变的。例如,数字5总是5,字符串"world"总是"world"。这种不变性是字面量被视为常量的直接原因。
  • 内存效率:在编译时,编译器知道字面量的值,并且可以进行优化。因为这些值是不变的,所以在程序的执行期间不需要为它们分配可变存储空间。
  • 安全性:将字面量视为常量可以防止程序意外改变它们的值,这增加了代码的安全性和预测性。

示例

考虑以下代码片段:

1
2
int x = 10;   // 10是一个整数字面量常量
char c = 'A'; // 'A'是一个字符字面量常量

在这些示例中,10'A'都是字面量常量。你不能去“改变”10的值变成11,或者改变'A''B',它们固有地代表了它们的值。你可以改变变量xc的值,但字面量10'A'本身的含义是不可改变的。

结论

理解字面量常量作为常量的原因,帮助我们在编写代码时更清楚地理解值的不变性和表达式的含义,这对于编写可读性高、稳定和高效的代码非常重要。



  1. constexpr 常量表达式
  • C++11引入了constexpr,用于定义编译时常量表达式,这意味着表达式的值在编译时就已经确定。
  • 可用于函数、对象的构造函数等更复杂的场景。
1
2
3
constexpr int square(int x) {
return x * x;
}
查看constexpr

constexpr是C++11引入的一个关键字,用于定义常量表达式。这意味着表达式的值在编译时就已经确定,而不是在程序运行时计算。使用constexpr可以让你声明变量、函数等为编译时常量,从而提高程序的性能和可靠性。

基本概念

constexpr指示编译器验证变量的值或函数的返回值是否可以在编译时确定。这对于优化很有帮助,因为编译器可以在编译期间计算出这些值,减少运行时的计算开销。

constexpr变量

一个constexpr变量必须立即初始化,并且其初始化表达式必须在编译时可求值。例如:

1
2
constexpr int max_size = 100;  // 正确,100是编译时常数
constexpr int limit = max_size + 1; // 正确,max_size是一个constexpr变量,因此max_size+1也是编译时常数

constexpr函数

constexpr函数能够在编译时对其输入进行计算,只要提供给它的参数是编译时常数。这意味着,你可以用constexpr函数初始化constexpr变量,或者在需要编译时常数表达式的上下文中使用它们。

constexpr函数有几个限制,例如在C++11和C++14中,函数体只能包含非常有限的代码。从C++14开始,这些限制放宽了,允许包含更多类型的语句。

1
2
3
4
5
constexpr int square(int x) {
return x * x;
}

constexpr int sq = square(9); // 编译时计算square(9)

const的区别

  • const用于定义常量,这意味着一旦赋值后,该变量的值就不能改变。但const变量的值不一定在编译时就已知。
  • constexpr表示表达式的值可以(并且必须)在编译时求值。这适用于变量、函数等。

使用场景

constexpr在需要编译时常量表达式,例如数组大小、整数模板参数等场合特别有用。它们还可以用于性能优化,因为constexpr允许在编译时而非运行时进行计算。

注意事项

  • constexpr函数不一定总是在编译时被求值。如果你在运行时用非常量表达式作为参数调用一个constexpr函数,该调用仍然可以工作,但计算将在运行时进行。
  • 使用constexpr要求编译器支持C++11或更高版本。

总之,constexpr是C++中一个强大的特性,它提供了一种在编译时而不是运行时进行计算的方法,有助于提高程序的效率和可靠性。



常量指针与指针常量

  • 常量指针(const指针):指向常量的指针,不能通过该指针修改所指向的值。
1
const int* ptr;
1
int const* ptr;        // 和上面是等价的
  • 指针常量:指针本身是常量,不能指向别的地址,但所指向的值可以修改。
1
int* const ptr = &var;
  • 同时使用:既不能修改指针指向,也不能通过指针修改值。
1
const int* const ptr = &var;

理解并正确使用常量是C++编程中的基本技能之一,有助于创建更安全、更健壮的程序。




2. 关键字

C++ Only
bool catch class const_cast
constexpr dynamic_cast explicit false
friend inline mutable namespace
new nullptr operator private
protected public reinterpret_cast static_cast
template this throw true
try typeid typename using
virtual wchar_t
Common
Keywords
auto break case char const
continue default do double else
enum extern float for goto
if int long register return
short signed sizeof static struct
switch typedef union unsigned void
volatile while
auto

C语言和C++中的auto关键字确实都存在,但它们的用途和含义随着C++11的引入发生了显著变化。

C语言中的auto

在C语言中,auto关键字用于声明自动局部变量。然而,由于局部变量默认就是自动(automatic)存储类别的,所以在实际C程序中,auto关键字几乎从未被显式使用。其用法主要是语法上的,而不带有实际的操作意义。

例如,以下两个声明在C中是等价的,auto关键字是多余的:

1
2
int var = 0;
auto int var = 0;

C++中的auto

在C++11及其之后的版本中,auto关键字的含义被重新定义用于类型推断。这意味着编译器会根据变量的初始值自动推断出变量的类型。这一变化极大地提高了代码的可读性和编写的便捷性,特别是对于复杂类型(如迭代器或者lambda表达式的类型)来说。

在C++11中,你可以这样使用auto

1
2
auto x = 5; // x 被推断为 int
auto y = 1.5; // y 被推断为 double

在这里,auto使得编码更加简洁,并且能够很容易地适应类型的变化,而不需要修改每一个变量的声明。

总结:

  • C语言中,auto是一个几乎不被使用的关键字,因为它指明了默认的存储类别(自动存储类别),而这是局部变量的默认行为。
  • C++ 中,auto关键字被重新定义,用于启用类型推断,极大地提升了编程的便利性和代码的通用性。

这种变化体现了C++对于简化编程复杂性和提高语言灵活性的持续努力。

volatile

volatile关键字在C和C++语言中被用来告诉编译器,一个变量的值可能以编译器不预期的方式被改变。这意味着使用volatile声明的变量不应该被编译器优化,因为它的值可能会在程序的控制流之外被修改。这个关键字主要用于两个场景:

  1. 硬件访问

当程序需要直接与硬件设备交互时,该设备的状态或数据可能会独立于程序的任何操作而改变。例如,一个硬件寄存器的值可能由外部事件(如硬件中断)而非程序本身的写操作所修改。在这种情况下,使用volatile可以防止编译器做出假设并错误地优化掉对这些外部状态的读取或写入操作。

  1. 多线程应用

在多线程应用程序中,一个线程可能修改另一个线程可以访问的变量。如果没有volatile,编译器可能会认为一个线程中的循环检查这个变量永远不会结束,因为在该线程的控制流中,变量看似没有被修改。使用volatile可以避免这样的优化,确保变量的读取或写入直接反映在内存中,从而允许线程安全地检查和修改变量。

使用示例

假设有一个表示硬件状态的变量,或者在多线程环境下共享的变量:

1
volatile int hardwareStatus;

这里,volatile关键字告诉编译器,hardwareStatus变量的值可能在程序执行过程中随时被外部因素改变,因此在每次访问时都需要直接从内存中读取它的值,而不能仅仅依赖于寄存器中的可能已经优化过的副本。

注意事项

虽然volatile对于上述用例非常重要,但它并不保证变量访问的原子性或线程间的同步。对于需要原子操作或同步的场景,应该使用专门的同步机制或原子操作,如互斥锁(mutexes)或C++11中的原子类型(std::atomic)。

总之,volatile关键字是编程中用于特定用途的低级工具,正确使用时可以确保程序能够正确地与硬件交互或处理多线程中的共享数据。然而,它并不是解决并发问题的万能钥匙,需要谨慎使用。





3. 数据类型

在64位机器上,C++基本数据类型的大小可能会根据不同的编译器和操作系统有所不同。然而,有一些通用的规则可以作为参考。以下是一些常见的C++数据类型在大多数64位环境中的典型大小:

类型 大小(字节)
bool 1
char 1
wchar_t 2 或 4
char16_t 2
char32_t 4
short 2
int 4
long 4 或 8
long long 8
float 4
double 8
long double 8 或 16
指针(如int* 8

几点需要注意的是:

  • wchar_t的大小依赖于编译器和平台。在Windows上通常是2字节,而在Linux和macOS上通常是4字节。
  • long类型在Windows的64位版本中仍然是4字节,而在大多数Unix-like系统(包括Linux和macOS)中,long是8字节。
  • long double的大小也依赖于编译器和平台,它可能是和double一样,或者更大,例如16字节。

以上信息提供了一个大致的指导,但最准确的方法是使用sizeof运算符直接在目标平台上进行检查。例如,你可以写一个简单的C++程序来打印不同类型的大小:

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
std::cout << "Size of int: " << sizeof(int) << " bytes\n";
std::cout << "Size of long: " << sizeof(long) << " bytes\n";
// 以此类推,打印其他类型的大小
return 0;
}

这样,你可以获得在你的特定编译器和操作系统上各种数据类型的确切大小。





4. 字面量

1
2
3
3.14    // double
3.14f // float
3e2 // double, 科学计数法,指数必须为整数

cout printf没有指定输出精度的话,最多输出6位有效数字(小数点前的也算有效数字)





5. 转义字符

转义字符 含义 ASCII码值(十进制)
\a 警报 007
\b 退格(BS) ,将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) (跳到下一个TAB位置) 009
\v 垂直制表(VT) 011
\ 代表一个反斜线字符"" 092
代表一个单引号(撇号)字符 039
" 代表一个双引号字符 034
? 代表一个问号 063
\0 数字0 000
\ddd 8进制转义字符,d范围0~7 3位8进制
\xhh 16进制转义字符,h范围0~9,a~f,A~F 3位16进制
怎么理解水平制表符

前面没有满八个位置,就补几个空格来满足

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <stdio.h>

using namespace std;

int main() {
printf("\tyy\n");
printf("1234\tyy\n");
printf("1234567\tyy\n");
printf("12345678\tyy\n");
printf("123456789\tyy\n");
return 0;
}

输出为:

1
2
3
4
5
6
        yy
1234 yy
1234567 yy
12345678 yy
123456789 yy





6. 宏定义

宏定义不一定非要写在源代码的开头。在C和C++中,宏是通过预处理器指令定义的,这意味着它们在编译之前被处理。宏定义可以出现在源文件的任何地方,但是它只会影响它定义之后的代码。因此,宏定义通常放在文件的开始部分,以确保它们在整个文件中都有效,这是一种常见的做法,但并非强制要求。

关键点

  • 作用域:宏的作用范围从定义点开始,到文件结束或被#undef指令取消定义。如果你希望一个宏在整个文件中都有效,将其定义在文件开头是有意义的。
  • 组织性:为了代码的清晰和易于维护,常常将宏定义放在文件顶部或专门的头文件中。这样做有助于其他开发者快速了解文件或项目中使用的宏。
  • 条件编译:有时,宏定义会根据条件编译指令来进行定义,这可能会出现在文件的任何位置。例如,根据不同的操作系统来定义不同的宏。

示例

宏定义可以在文件的任何位置进行,只要在使用之前定义即可:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main() {
printf("Hello, world!\n");

#define MSG "Defined after using."

printf("%s\n", MSG); // 使用宏定义
return 0;
}

在这个例子中,MSG宏定义在main函数内部,仅在定义之后的代码中有效。如果尝试在定义之前使用MSG,编译器会报错,因为在那个点上它还没有被定义。

结论

尽管宏定义可以写在源代码的任何位置,但将它们组织在文件开头或在专门的头文件中是一种更清晰、更易于管理的做法。这种组织方式可以提高代码的可读性和可维护性。

#undef

#undef 指令在C和C++中用于取消宏定义。当你不再需要一个宏,或者想要重新定义它时,#undef 可以用来移除当前的宏定义。这样做可以防止宏的意外重定义,或者在特定的代码区域内限制宏的作用域。

使用方式:

使用 #undef 非常简单,只需要跟上你想要取消定义的宏的名称。例如:

1
2
3
4
5
#define PI 3.14159
// 使用PI...

#undef PI
// 从这一点开始,PI不再定义

示例:

假设你有一个宏,用于在调试模式下打印日志信息,但在发布版本中你不希望包含这些日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define DEBUG // 开启调试模式

#ifdef DEBUG
#define LOG(msg) printf("Log: %s\n", msg)
#else
#define LOG(msg)
#endif

// 使用LOG宏...
LOG("This is a debug message.");

#undef LOG
// 从这一点开始,LOG宏不再可用

在这个例子中,LOG 宏根据是否定义了 DEBUG 来改变行为。通过在不需要它之后使用 #undef,可以确保它不会在后续的代码中被误用。

注意事项:

  • 使用 #undef 取消宏定义后,如果需要再次使用相同的宏名称,你必须重新定义它。
  • #undef 对后续的代码块生效,对其前面的代码没有影响。
  • 在大型项目中,谨慎使用宏定义和 #undef,因为它们如果被滥用,可能会使代码难以理解和维护。
  • 尽管 #undef 可以用来限制宏的作用域,但在现代C++中,更推荐使用常量、内联函数或者模板来代替宏,以提高代码的安全性和清晰度。
#define _CRT_SECURE_NO_WARNINGS

#define _CRT_SECURE_NO_WARNINGS这种用法与我们通常定义宏来创建常量或宏函数略有不同。它不是用来直接替换代码中的文本,而是作为一种特殊的标记,用于影响编译器的行为。这类宏定义主要用于配置编译环境或控制编译器的特定行为,而不是用于代码中的直接替换。

常规宏定义

常规的宏定义通常有以下两种形式:

  1. 定义常量:用宏来定义一个常量值,例如:

    1
    #define PI 3.14159

    在这里,每当预处理器遇到PI,它都会用3.14159来替换它。

  2. 定义宏函数:定义一个宏函数,允许在代码中使用类似函数调用的语法来执行代码替换,例如:

    1
    #define SQUARE(x) ((x) * (x))

    这里,SQUARE(x)会被替换为计算x平方的表达式。

特殊的宏定义

与常规宏定义不同,#define _CRT_SECURE_NO_WARNINGS这样的宏并不直接影响代码的内容替换,而是用作编译器的配置指示。这说明宏定义在C和C++中的应用非常灵活,不仅限于文字替换:

  • 控制编译器行为:某些宏用于开启或关闭编译器的特定功能,如_CRT_SECURE_NO_WARNINGS
  • 配置编译环境:通过定义或取消定义宏来启用或禁用代码中的特定部分,通常用于跨平台兼容性或条件编译。
  • 平台特定的定义:操作系统或编译器可能会预定义一些宏,用于标识平台特性或编译环境。

结论

#define _CRT_SECURE_NO_WARNINGS与常规宏定义的主要区别在于它的目的和效果。它不是用来替换代码中的文本,而是用来配置编译器的行为,避免在使用旧版C运行时函数时产生的安全警告。这种类型的宏定义体现了宏的另一种强大用途,即控制编译过程和环境的配置,而不仅仅是代码中的文本替换。



C和C++中的预处理器指令不仅限于#define,还包括一系列其他有用的指令,它们在编译之前处理源代码,用于条件编译、文件包含、宏定义等。以下是一些常用的预处理器指令:

  1. #include

用于包含头文件,将文件的内容直接插入到当前位置。有两种形式:

  • #include <filename>:用于标准库头文件。
  • #include "filename":用于项目内的头文件。
  1. #define

定义宏,可以是对象(常量值)或函数(带参数的宏)。

  1. #undef

取消已定义的宏的定义。

  1. #if, #elif, #else, #endif

用于条件编译。根据条件是否满足,决定是否编译代码段。

  1. #ifdef, #ifndef

检查宏是否被定义:

  • #ifdef:如果宏已定义,则编译下面的代码。
  • #ifndef:如果宏未定义,则编译下面的代码。
  1. #error

当遇到此指令时,预处理器会显示一个错误消息并停止编译。

  1. #pragma

提供了一种标准化的方法,用于向编译器传递特定的指令。编译器可能会忽略它不认识的#pragma指令。

  1. #line

允许改变编译器的当前行号和文件名,对调试和错误报告很有用。

使用示例:

  • #include 示例

    1
    2
    #include <iostream>
    #include "myheader.h"
  • #define 和 #undef 示例

    1
    2
    #define PI 3.14159
    #undef PI
  • 条件编译示例

    1
    2
    3
    4
    #define DEBUG
    #ifdef DEBUG
    std::cout << "Debug mode is on." << std::endl;
    #endif
  • #error 示例

    1
    2
    3
    #ifndef REQUIRED_MACRO
    #error "REQUIRED_MACRO is not defined"
    #endif

预处理器指令是C和C++编程中强大的工具,允许程序员对编译过程有更细致的控制,包括代码的条件编译、配置管理和错误检测等。正确和有效地使用这些指令可以提高代码的可移植性、灵活性和可维护性。






7. 分文件编写

C++和C语言在分文件编写方面有很多相似之处,因为C++在很大程度上保持了与C的兼容性。然而,由于C++引入了类、模板和命名空间等新特性,因此C++分文件编写时还需要考虑这些特性的特定需求。下面是一些基本原则和区别:

相似之处

  1. 头文件(.h或.hpp):在C和C++中,头文件用于声明函数、变量、类等。它们允许在多个源文件之间共享这些声明,避免重复代码。

  2. 源文件(.c或.cpp):源文件包含函数、类方法的实现代码。在分文件编写中,头文件被包含在源文件中,以提供必要的声明。

  3. 预处理指令#include预处理指令在C和C++中都用于包含头文件,这使得函数、类的声明在源文件中可用。

C++特有的考虑因素

  1. 类声明与定义:在C++中,类通常在头文件中声明,在源文件中定义(实现)。这意味着类的成员函数在头文件中被声明,在一个或多个源文件中被定义。

  2. 模板:C++的模板(包括类模板和函数模板)通常在头文件中完全定义。由于模板需要在编译时实例化,它们的定义(实现)通常与声明放在同一个头文件中,而不是分开到源文件中。

  3. 内联函数:C++允许函数在头文件中被声明为inline,这样做的目的是减少小函数的调用开销。虽然内联函数也可以在C中通过宏实现,但C++的inline关键字提供了类型安全和更好的编程风格。

  4. 命名空间:C++支持命名空间,这是C不具备的特性。命名空间可以用于头文件和源文件中,以避免命名冲突。

示例

  • C++头文件(myclass.hpp)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // myclass.hpp
    #ifndef MYCLASS_HPP
    #define MYCLASS_HPP

    class MyClass {
    public:
    void myFunction();
    };

    #endif
  • C++源文件(myclass.cpp)

    1
    2
    3
    4
    5
    6
    // myclass.cpp
    #include "myclass.hpp"

    void MyClass::myFunction() {
    // 实现
    }

结论

虽然C++和C在分文件编写方面有很多相似之处,但C++的一些高级特性(如类、模板和命名空间)引入了额外的考虑因素。理解这些差异有助于有效地组织和维护C++项目的代码。



确实,你可以通过extern关键字在C或C++中实现不同源文件间的函数或变量的共享,而不必依赖于头文件。extern用于声明一个变量或函数是在别处定义的,这样做可以在不同的源文件之间共享同一个全局变量或函数。不过,这种方式相比于使用头文件来说,管理起来更为繁琐,特别是在大型项目中,而且容易出错。

使用extern分文件编写的基本方法

假设你有两个源文件,file1.cppfile2.cpp,你想在file2.cpp中使用file1.cpp定义的变量或函数。

file1.cpp

1
2
3
4
5
6
7
#include <iostream>

void functionFromFirstFile() {
std::cout << "Function in file 1" << std::endl;
}

int globalVar = 42; // 全局变量定义

file2.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>

extern void functionFromFirstFile(); // 函数声明
extern int globalVar; // 外部变量声明

int main() {
functionFromFirstFile();
std::cout << "Global variable: " << globalVar << std::endl;
return 0;
}

注意事项

  • 维护性:随着项目规模的增大,如果不使用头文件而仅依赖于extern声明,每次外部符号的定义变更时,都需要手动更新所有引用了这些符号的文件。这大大增加了维护的难度和出错的几率。
  • 可读性和组织性:使用头文件不仅可以使项目结构更清晰,还可以通过条件编译、宏等预处理指令提供更多的灵活性和控制。
  • 重复声明问题:对于extern变量,如果在多个文件中需要访问,每个文件都需要重复声明,增加了冗余。

结论

虽然理论上可以通过extern关键字在没有头文件的情况下进行分文件编写,但这种做法并不推荐,特别是对于中大型项目。使用头文件可以更好地组织代码,减少重复,提高代码的可读性和可维护性。在实际开发中,头文件是分文件编程的最佳实践之一





8. 指针

  • 概念

指针的作用: 可以通过指针间接访问内存

指针变量和普通变量的区别

  • 普通变量存放的是数据,指针变量存放的是地址
  • 指针变量可以通过" * "操作符,操作指针变量指向的内存空间,这个过程称为解引用
指针和指针变量

指针和指针变量这两个术语经常被互换使用,但如果我们要区分它们,可以这样理解:

指针(Pointer)

  • 概念上的指针:指针本质上是一个内存地址,指向存储数据的某个位置。它是编程语言中的一个抽象概念,用于表示访问其他变量的能力。
  • 在许多情况下,当我们谈论指针时,我们实际上是在讨论存储内存地址的变量或者某种方式上的引用,这可以是一个具体的内存地址,也可以是通过某种方式(如通过指针变量)获得的内存地址。

指针变量(Pointer Variable)

  • 实体上的指针:指针变量是一种特殊类型的变量,用于存储内存地址。这意味着,指针变量具有存储其他变量地址的能力,通过这个地址,可以访问或修改该地址处的数据。
  • 指针变量是具体的、可以操作的实体。在C或C++代码中,你可以声明指针变量,给它赋值(一个地址),并通过它访问或修改它指向的数据。

区别和联系

  • 区别:从最严格的意义上讲,指针是一个内存地址,而指针变量是存储这个内存地址的变量。因此,指针是一个更广泛的概念,而指针变量是这个概念在程序中的具体实现。
  • 联系:在实际应用中,这两个术语经常被互换使用,因为指针变量是实现指针概念的主要方式。当你声明一个指针变量并操作它时,你实际上是在操作指针,即内存地址。

示例

1
2
3
4
5
6
int main() {
int value = 5; // 一个普通的整型变量
int* ptr = &value; // ptr 是一个指针变量,存储了 value 的地址
// 在这里,ptr 就是指针变量,&value 是一个指针(内存地址)
return 0;
}

在上面的示例中,ptr是一个指针变量,因为它是一个变量,用于存储value变量的地址。而&value产生一个指向value的指针,即value的内存地址。在日常使用中,指针和指针变量这两个术语往往可以互换,尤其是在上下文清楚地表明了讨论的是变量还是它所存储的地址时。



  • 指针所占的内存空间

C++中指针的大小通常与所在平台的机器字长(或地址总线宽度)相匹配。机器字长指的是CPU一次性可以处理数据的位数,它直接关联到操作系统可以直接寻址的内存空间大小。在32位系统上,指针的大小通常是32位(4字节),而在64位系统上,指针的大小通常是64位(8字节)。

这样设计的主要原因是为了使指针能够有效地访问整个可寻址的内存空间。在32位系统上,4字节指针可以寻址最多(2^{32})个地址,即4GB的内存空间。在64位系统上,8字节指针可以寻址最多(2^{64})个地址,理论上可以达到16EB(Exabytes,百万TB)的内存空间,尽管实际上由于硬件和操作系统的限制,并不能全部使用这么多内存。

注意事项

  • 尽管指针大小通常与机器字长相匹配,但这不是一个由C++语言标准强制规定的规则。语言标准没有指定指针的具体大小,这取决于编译器和目标平台的实现。
  • 指针类型的大小(无论是指向整型、浮点型还是类对象的指针)在特定平台上通常是相同的,但指针与指向的数据类型的大小无关。即,一个指向int的指针和一个指向double的指针,在同一个平台上大小是相同的。
  • 函数指针的大小也通常与其他类型的指针相同,但在某些特殊的架构或编译器设置中,它们的大小可能会有所不同。

总的来说,虽然在大多数情况下,指针的大小确实等于机器字长,但这是基于特定平台的约定和实现,而非C++语言标准的强制要求。

验证

VS studio2017 这里可以修改:

x86(32位机器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

int main() {
cout << sizeof(char *) << endl;
cout << sizeof(int *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(double *) << endl;
return 0;
}

/*
输出为
4
4
4
4

*/

x64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

int main() {
cout << sizeof(char *) << endl;
cout << sizeof(int *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(double *) << endl;
return 0;
}

/*
输出为
8
8
8
8

*/


  • 空指针

指针变量指向内存中编号为0的空间

用途: 初始化指针变量

注意: 空指针指向的内存是不可以访问的

NULL 还是 nullptr?

在C++中,nullptrNULL都可以用来表示空指针,但它们之间存在一些重要的区别,特别是在C++11及以后的版本中。

NULL

  • NULL在C++(和C)程序中长期被用作表示空指针的值。在C++中,NULL通常被定义为0,基于C++对整数到指针隐式转换的支持,这意味着将整数0赋值给指针变量时,它会被视为一个空指针。
  • 使用NULL可能会引起一些混淆和问题,特别是在函数重载的情况下,因为NULL实际上是整数0,它可以与整数类型匹配,这可能不是预期的行为。

nullptr

  • nullptr是C++11引入的一个新的关键字,专门用来表示空指针。它的类型是nullptr_t,可以自动转换为任何原生指针类型,但不会被错误地转换为整数类型。
  • 使用nullptr可以提高代码的清晰度和安全性,特别是在涉及函数重载和模板函数的场景中,它能够明确表示空指针,避免与整数0混淆。

推荐使用nullptr

鉴于nullptr提供了类型安全的空指针表示,以及更好的兼容性和清晰性,推荐在C++11及以后的版本中使用nullptr来表示空指针。这样可以减少潜在的混淆,尤其是在涉及重载和模板的复杂情况下。

示例

1
2
3
4
5
6
7
8
9
10
11
12
void func(int) {
// 处理整数
}

void func(char*) {
// 处理字符指针
}

int main() {
func(NULL); // 可能调用 func(int),这取决于NULL的定义
func(nullptr); // 明确调用 func(char*)
}

在上面的例子中,使用nullptr可以确保调用正确的func函数版本,而使用NULL可能会产生歧义。



  • 野指针
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

using namespace std;

int main() {
//指针变量p指向内存地址编号为0x1100的空间
int * p = (int *)0x1100;

//访问野指针报错
cout << *p << endl;
return 0;
}

总结:空指针和野指针都不是我们申请的空间,因此不要访问。



  • const修饰指针

常量指针与指针常量

  • 常量指针(const指针):指向常量的指针,不能通过该指针修改所指向的值。
1
const int* ptr;
1
int const* ptr;        // 和上面是等价的
  • 指针常量:指针本身是常量,不能指向别的地址,但所指向的值可以修改。
1
int* const ptr = &var;
  • 同时使用:既不能修改指针指向,也不能通过指针修改值。
1
const int* const ptr = &var;


  • 智能指针

C++现代实用教程:智能指针_哔哩哔哩_bilibili



  • more

更多指针内容可以查看本站文章C语言笔记 | GXBLOGS:【目录】随手记>指针








C++核心知识

1. 内存

是的,C++的内存模型基本上和C语言非常相似,因为C++是以C为基础构建的,并保留了C的很多特性和概念。C++程序的内存布局通常包括以下几个区域:

  1. 栈(Stack)
  • 用于存放函数的局部变量、函数参数以及函数的返回地址等。栈是自上而下扩展的数据结构,具有后进先出的特性。当函数调用时,新的栈帧(包含函数的局部变量和其他上下文信息)被推入栈中;当函数返回时,栈帧被弹出。
  1. 堆(Heap)
  • 用于动态内存分配。与栈不同,堆是由程序员通过new(在C++中)或malloc(在C中)等操作手动分配和释放的。堆的大小不是固定的,可以动态扩展或缩小。
  1. 全局/静态存储区(Global/Static Area)
  • 存放全局变量和静态变量。这些变量在程序的整个生命周期内都存在。在C++中,全局对象的构造和静态对象的初始化发生在main函数执行之前,它们的析构/销毁发生在main函数执行结束后。
  1. 常量存储区(Constants Area)
  • 存放常量数据,如字符串字面值和其他常量表达式。这部分通常是只读的,在某些编译器实现中,它可能是.rodata(Read-Only Data)段的一部分。
  1. 代码区(Code Area)
  • 存放程序的二进制代码,即编译后的机器指令。这部分通常也是只读的,防止程序的指令在运行时被意外或恶意修改。

C++的一些特殊考虑

虽然C++和C在内存布局方面非常相似,但C++引入了对象模型和RAII(Resource Acquisition Is Initialization)等概念,使得内存管理更加复杂。例如:

  • 对象的内存管理:C++通过构造函数和析构函数自动管理对象的生命周期,支持对象的动态创建(在堆上)和自动销毁。
  • 智能指针:C++提供了智能指针如std::unique_ptrstd::shared_ptr等,以便更安全地管理堆上的内存。

总的来说,C++保留了C语言的内存模型,并在此基础上增加了面向对象和自动资源管理等特性,使得内存管理更为灵活和安全。

malloc和new

在C++中,mallocnew都可以用来在堆上分配内存,但它们之间存在一些关键的区别,包括它们分配的内存区域的处理方式。以下是mallocnew最主要的区别:

  1. 构造函数和析构函数
  • new:分配内存的同时会调用对象的构造函数,为对象的初始化提供了机会。相应地,delete会调用对象的析构函数,确保资源得到释放。
  • malloc:仅仅分配内存,不会调用构造函数。因此,当使用malloc分配内存给对象时,对象的成员不会被自动初始化。相应地,free释放内存但不会调用析构函数。
  1. 类型安全
  • new:返回分配对象的正确类型的指针,不需要类型转换。
  • malloc:返回void*类型的指针,需要显式转换为目标类型的指针。
  1. 错误处理
  • new:在内存分配失败时,会抛出std::bad_alloc异常(除非使用了new (nothrow))。
  • malloc:在内存分配失败时,返回NULL
  1. 分配数组
  • 使用new[]delete[]可以为数组分配和释放内存,同时为数组中的每个对象调用构造函数和析构函数。
  • mallocfree则没有这样的能力,它们仅仅分配和释放原始内存,不负责对象的构造和析构。
  1. 内存分配区域

尽管mallocnew在技术上都是在堆区分配内存,但实际上C++标准并没有严格规定newmalloc分配的内存区域必须是同一个。在大多数实现中,它们都是从同一块堆内存中分配内存的,但new可能涉及更复杂的内存管理策略(例如,使用内存池)。因此,newmalloc分配的内存区域在表现上可能有细微的不同,尤其是在处理对象构造和析构时。

结论

尽管mallocnew都用于内存分配,但由于newdelete提供了类型安全、自动调用构造函数和析构函数的特性,它们是C++推荐的内存分配和释放方式。使用newdelete可以让代码更加安全、简洁和面向对象。





2. new

在C++中,new关键字用于动态分配内存,并根据需要进行对象的构造。与C语言的malloc函数相比,new不仅分配内存,还会自动调用对象的构造函数(如果分配的是一个对象),使得内存分配和对象初始化一步到位。此外,new还提供了类型安全,确保分配的内存类型与所需类型匹配。

基本用法:

1
2
int* ptr = new int; // 分配一个int大小的内存,并返回指向这块内存的指针
*ptr = 5; // 在分配的内存中存储值5

在上面的例子中,new int动态分配了足够存储一个int类型值的内存,并返回一个指向这块内存的指针。与此同时,分配的内存被自动初始化。

带有初始化的用法:

new也支持在分配内存时进行初始化:

1
int* ptr = new int(10); // 分配并初始化为10

对于自定义类型:

1
2
3
4
5
6
7
8
class MyClass {
public:
MyClass(int val) : value(val) {}
private:
int value;
};

MyClass* myObject = new MyClass(10); // 分配内存并调用构造函数

数组的分配:

new还可以用于分配对象数组:

1
int* array = new int[10]; // 分配一个10个int的数组

数组分配时不支持直接初始化每个元素,但你可以使用后面的括号来初始化POD(Plain Old Data,简单旧数据类型)类型数组的元素为零:

1
int* array = new int[10](); // 分配并初始化为0

delete关键字

分配的动态内存必须手动释放,以避免内存泄露。这是通过delete关键字(对于单个对象)或delete[]关键字(对于对象数组)来完成的:

1
2
delete ptr; // 释放单个对象
delete[] array; // 释放对象数组

异常处理:

new无法分配足够的内存时,默认行为是抛出一个std::bad_alloc异常。这可以通过异常处理机制来捕获并处理:

1
2
3
4
5
try {
MyClass* myObject = new MyClass;
} catch (const std::bad_alloc& e) {
// 处理内存分配失败
}

还有一个nothrow版本的new,在内存分配失败时,不抛出异常,而是返回nullptr

1
2
3
4
int* ptr = new(std::nothrow) int;
if (!ptr) {
// 处理内存分配失败
}

总结:

new关键字在C++中是用于动态内存分配的强大工具,提供了类型安全、自动对象初始化以及异常处理等机制。与delete一起使用,它们构成了C++中动态内存管理的基础。使用newdelete时,开发者需要负责匹配地使用它们,以确保每次分配的内存都被适时释放,避免内存泄漏。



  • 实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>

using namespace std;

//全局变量
int g_a = 10;
int g_b = 10;

//全局常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {

//局部变量
int a = 10;
int b = 10;

//打印地址
cout << "局部变量a地址为: " << (int)&a << endl;
cout << "局部变量b地址为: " << (int)&b << endl;

cout << "全局变量g_a地址为: " << (int)&g_a << endl;
cout << "全局变量g_b地址为: " << (int)&g_b << endl;

//静态变量
static int s_a = 10;
static int s_b = 10;

cout << "静态变量s_a地址为: " << (int)&s_a << endl;
cout << "静态变量s_b地址为: " << (int)&s_b << endl;

cout << "字符串常量地址为: " << (int)&"hello world" << endl;
cout << "字符串常量地址为: " << (int)&"hello world1" << endl;

cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl;
cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;

const int c_l_a = 10;
const int c_l_b = 10;
cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;

system("pause");

return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10
11
12
局部变量a地址为: 9828232
局部变量b地址为: 9828220
全局变量g_a地址为: 16236544
全局变量g_b地址为: 16236548
静态变量s_a地址为: 16236552
静态变量s_b地址为: 16236556
字符串常量地址为: 16227316
字符串常量地址为: 16227332
全局常量c_g_a地址为: 16227120
全局常量c_g_b地址为: 16227124
局部常量c_l_a地址为: 9828208
局部常量c_l_b地址为: 9828196




3. 智能指针

来自:C++ 智能指针 - 全部用法详解-CSDN博客

3.1 为什么使用智能指针

一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏

如下例子就是内存泄露的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>
#include <memory>

using namespace std;


// 动态分配内存,没有释放就return
void memoryLeak1() {
string *str = new string("动态分配内存!");
return;
}

// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
string *str = new string("内存泄露!");

// ...此处省略一万行代码

// 发生某些异常,需要结束函数
if (1) {
return -1;
}
// 另外,使用try、catch结束函数,也会造成内存泄漏!


delete str; // 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
return 1;
}


int main(void) {

memoryLeak1();

memoryLeak2();

return 0;
}

memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!

  1. C++98 提供了 auto_ptr 模板的解决方案
  2. C++11 增加 unique_ptrshared_ptrweak_ptr




3.2 auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用 delete 来释放内存!


  • 用法:

头文件: #include < memory >
用 法: auto_ptr<类型> 变量名(new 类型)

例如:

1
2
auto_ptr< string > str(new string(“我要成为大牛~ 变得很牛逼!”));
auto_ptr<vector< int >> av(new vector< int >());

我们先定义一个类,类的构造函数和析构函数都输出一个字符串用作提示!
定义一个私有成员变量,赋值20.
再定义一个私有成员方法用于返回这个私有成员变量。

1
2
3
4
5
6
7
8
9
10
11
class Test {
public:
Test() { cout << "Test的构造函数..." << endl; }
~Test() { cout << "Test的析构函数..." << endl; }

int getDebug() { return this->debug; }

private:
int debug = 20;
};

当我们直接new这个类的对象,却没有释放时。。。

1
2
3
4
5
int main(void) {
Test *test = new Test;

return 0;
}

可以看到,只是打印了构造函数这个字符串,而析构函数的字符却没有被打印,说明并没有调用析构函数!这就导致了内存泄露!
解决内存泄露的办法,要么手动delete,要么使用智能指针!


  • 使用智能指针
1
2
// 定义智能指针
auto_ptr<Test> test(new Test);

智能指针可以像普通指针那样使用:

1
2
cout << "test->debug:" << test->getDebug() << endl;
cout << "(*test).debug:" << (*test).getDebug() << endl;

再看看输出结果:

1
2
3
4
5
6
7
8
9
10
int main(void) {

//Test *test = new Test;
auto_ptr<Test> test(new Test);

cout << "test->debug:" << test->getDebug() << endl;
cout << "(*test).debug:" << (*test).getDebug() << endl;

return 0;
}

为什么智能指针可以像普通指针那样使用???
因为其里面重载了 *-> 运算符, * 返回普通对象,而 -> 返回指针对象。

具体原因不用深究,只需知道他为什么可以这样操作就像!
函数中返回的是调用get()方法返回的值,那么这个get()是什么呢?

智能指针的三个常用函数:

  1. get() 获取智能指针托管的指针地址

    1
    2
    3
    4
    5
    // 定义智能指针
    auto_ptr<Test> test(new Test);

    Test *tmp = test.get(); // 获取指针返回
    cout << "tmp->debug:" << tmp->getDebug() << endl;

    但我们一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
    函数原型

    1
    2
    3
    4
    _NODISCARD _Ty * get() const noexcept
    { // return wrapped pointer
    return (_Myptr);
    }
  2. release() 取消智能指针对动态内存的托管

    1
    2
    3
    4
    5
    // 定义智能指针
    auto_ptr<Test> test(new Test);

    Test *tmp2 = test.release(); // 取消智能指针对动态内存的托管
    delete tmp2; // 之前分配的内存需要自己手动释放

    也就是智能指针不再对该指针进行管理,改由管理员进行管理!
    函数原型

    1
    2
    3
    4
    5
    6
    _Ty * release() noexcept
    { // return wrapped pointer and give up ownership
    _Ty * _Tmp = _Myptr;
    _Myptr = nullptr;
    return (_Tmp);
    }
  3. reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

    1
    2
    3
    4
    5
    6
    // 定义智能指针
    auto_ptr<Test> test(new Test);

    test.reset(); // 释放掉智能指针托管的指针内存,并将其置NULL

    test.reset(new Test()); // 释放掉智能指针托管的指针内存,并将参数指针取代之

    reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
    函数原型

    1
    2
    3
    4
    5
    6
    void reset(_Ty * _Ptr = nullptr)
    { // destroy designated object and store new pointer
    if (_Ptr != _Myptr)
    delete _Myptr;
    _Myptr = _Ptr;
    }

  • 使用建议
  1. 尽可能不要将auto_ptr 变量定义为全局变量或指针;

    1
    2
    // 没有意义,全局变量也是一样
    auto_ptr<Test> *tp = new auto_ptr<Test>(new Test);
  2. 除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针;

    1
    2
    3
    auto_ptr<Test> t1(new Test);
    auto_ptr<Test> t2(new Test);
    t1 = t2; // 不要这样操作...
    why?

    这段代码展示了std::auto_ptr的一个特别之处,这个特别之处在于它的拷贝行为。当你执行t1 = t2;这一操作时,你实际上是在做两件事:

    1. 转移所有权std::auto_ptr有一个所谓的“独占”所有权模型,意味着一个auto_ptr对象在任何时刻只能拥有一个对动态分配对象的所有权。当你将一个auto_ptr赋值给另一个时(在这里是t1 = t2;),你实际上将t2所拥有的对象的所有权转移给了t1。在赋值后,t1将指向t2之前所拥有的对象,而t2将变为null。

    2. 可能导致悬垂指针:由于t2的所有权被转移了,t2现在是空的(null)。如果你在赋值操作之后试图使用t2,你将访问一个null指针,这可能导致未定义的行为。更糟糕的是,如果t1之前已经拥有一个对象的所有权,那么这次赋值操作将导致t1原来指向的对象被删除(因为auto_ptr在转移所有权时会删除自己原来管理的对象),这就可能导致原来的资源泄露,如果还有其他指针指向这个被t1释放的对象,它们就变成了悬垂指针。

    因此,使用std::auto_ptr时,需要非常小心地管理所有权,以避免资源泄漏和悬垂指针的问题。正是由于这种复杂且容易出错的所有权管理,std::auto_ptr已经在C++11标准中被废弃,并被更现代且安全的智能指针如std::unique_ptrstd::shared_ptr所取代。这些现代智能指针提供了更清晰、更安全的所有权语义,使得资源管理更加简单和直观。

  3. C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。

  4. auto_ptr 被C++11抛弃的主要原因

    1). 复制或者赋值都会改变资源的所有权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // auto_ptr 被C++11抛弃的主要原因
    auto_ptr<string> p1(new string("I'm Li Ming!"));
    auto_ptr<string> p2(new string("I'm age 22."));

    cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;

    // p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
    // 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
    p1 = p2;
    cout << "p1 = p2 赋值后:" << endl;
    cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;

    2). 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    vector<auto_ptr<string>> vec;
    auto_ptr<string> p3(new string("I'm P3"));
    auto_ptr<string> p4(new string("I'm P4"));

    // 必须使用std::move修饰成右值,才可以进行插入容器中
    vec.push_back(std::move(p3));
    vec.push_back(std::move(p4));

    cout << "vec.at(0):" << *vec.at(0) << endl;
    cout << "vec[1]:" << *vec[1] << endl;


    // 风险来了:
    vec[0] = vec[1]; // 如果进行赋值,问题又回到了上面一个问题中。
    cout << "vec.at(0):" << *vec.at(0) << endl;
    cout << "vec[1]:" << *vec[1] << endl;

    3). 不支持对象数组的内存管理

    1
    auto_ptr<int[]> array(new int[5]);    // 不能这样定义

所以,C++11用更严谨的unique_ptr 取代了auto_ptr





3.3 unique_ptr

auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr主要有三大问题:

  1. 复制和赋值会改变资源的所有权,不符合人的直觉。
  2. 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
  3. 不支持对象数组的操作

以上问题已经在上面体现出来了,下面将使用unique_ptr解决这些问题。

所以,C++11用更严谨的unique_ptr 取代了auto_ptr

unique_ptrauto_ptr用法几乎一样,除了一些特殊。

std::move()

std::move()是C++11及之后标准中引入的一个标准库函数,它的作用是将其参数转换为右值引用。这个函数本身并不移动任何东西;它仅仅是一个类型转换函数,允许在特定的情况下启用移动语义。

移动语义的基础

在C++中,对象通常通过两种方式传递:复制和移动。复制是创建一个对象的副本,而移动是将资源从一个对象转移到另一个,通常在原对象不再需要这些资源的情况下进行。移动语义是C++11引入的一个特性,它允许资源的高效转移,减少了不必要的复制,从而提高程序的性能。

std::move()的工作原理

  • 类型转换std::move()将其参数转换为对应的右值引用类型(T&&),这个转换提示编译器该对象可以安全地进行移动操作。这意味着,在调用std::move()后,可以使用该对象的移动构造函数或移动赋值操作符(如果它们被定义的话)。
  • 启用移动操作:通过将对象转换为右值引用,std::move()允许开发者在特定情况下显式地表示一个对象可以被"移动"。这是通过调用对象的移动构造函数或移动赋值操作符来实现的,这些操作通常比复制构造函数或赋值操作符更高效,因为它们仅涉及资源的转移而不是复制。

使用场景和注意事项

  • 所有权转移:当你想将一个对象的资源转移到另一个对象,并且原对象之后不再需要这些资源时,可以使用std::move()
  • 临时对象和返回值:在处理临时对象或函数返回值时,编译器通常会自动利用移动语义。在这种情况下,通常不需要显式使用std::move()
  • 谨慎使用:使用std::move()后,原对象处于一个有效但不可预测的状态。这意味着,除非对象被销毁或赋予新值,否则不应该再使用该对象。因此,std::move()应该谨慎使用,确保移动操作后不会再错误地使用被移动的对象。

通过这样的机制,std::move()在C++中扮演着重要的角色,使得开发者能够更有效地控制对象的资源管理和性能优化。


  • unique_ptr特性
  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的

A. 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unique_ptr<string> p1(new string("I'm Li Ming!"));
unique_ptr<string> p2(new string("I'm age 22."));

cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

// p1 = p2; // 禁止左值赋值
// unique_ptr<string> p3(p2); // 禁止左值赋值构造

unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2); // 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样

cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

B. 在 STL 容器中使用unique_ptr,不允许直接赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector<unique_ptr<string>> vec;
unique_ptr<string> p3(new string("I'm P3"));
unique_ptr<string> p4(new string("I'm P4"));

vec.push_back(std::move(p3));
vec.push_back(std::move(p4));

cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;

// vec[0] = vec[1]; /* 不允许直接赋值 */
vec[0] = std::move(vec[1]); // 需要使用move修饰,使得程序员知道后果

cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;
// 当然,运行后是直接报错的,因为vec[1]已经是NULL了,再继续访问就越界了。

C. 支持对象数组的内存管理

1
2
// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]); // 支持这样定义

除了上面ABC三项外,unique_ptr的其余用法都与auto_ptr用法一致。

  1. 构造

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class Test {
    public:
    Test() { cout << "Test的构造函数..." << endl; }
    ~Test() { cout << "Test的析构函数..." << endl; }

    void doSomething() { cout << "do something......" << endl; }
    };


    // 自定义一个内存释放器
    class DestructTest {
    public:
    void operator()(Test *pt) {
    pt->doSomething();
    delete pt;
    }
    };

    // unique_ptr<T> up; 空的unique_ptr,可以指向类型为T的对象
    unique_ptr<Test> t1;

    // unique_ptr<T> up1(new T()); 定义unique_ptr,同时指向类型为T的对象
    unique_ptr<Test> t2(new Test);

    // unique_ptr<T[]> up; 空的unique_ptr,可以指向类型为T[的数组对象
    unique_ptr<int[]> t3;

    // unique_ptr<T[]> up1(new T[]); 定义unique_ptr,同时指向类型为T的数组对象
    unique_ptr<int[]> t4(new int[5]);

    // unique_ptr<T, D> up(); 空的unique_ptr,接受一个D类型的删除器D,使用D释放内存
    unique_ptr<Test, DestructTest> t5;

    // unique_ptr<T, D> up(new T()); 定义unique_ptr,同时指向类型为T的对象,接受一个D类型的删除器D,使用删除器D来释放内存
    unique_ptr<Test, DestructTest> t6(new Test);
    删除器(Deleter)

    删除器(Deleter)是与智能指针一起使用的一个对象或函数指针,用来自定义智能指针释放其所拥有资源的方式。在std::unique_ptr的上下文中,删除器提供了一种机制,允许开发者指定当智能指针被销毁时应如何清理其所管理的资源。这是非常有用的,特别是当你需要对一些非标准资源进行管理时,例如打开的文件句柄、数据库连接、或者是需要调用特殊函数来释放的内存。

    在标准用法中,std::unique_ptr默认使用deletedelete[]来释放其管理的对象或数组。但是,通过提供一个自定义删除器,你可以覆盖这一默认行为,实现资源的特殊管理策略。

    如何使用删除器

    在你的示例中,DestructTest类被定义为一个删除器。它重载了operator(),使其能接受一个指向Test类型对象的指针。当unique_ptr<Test, DestructTest>需要释放其管理的对象时,它会调用这个删除器,而不是直接使用delete

    1
    2
    3
    4
    5
    6
    7
    class DestructTest {
    public:
    void operator()(Test *pt) {
    pt->doSomething();
    delete pt;
    }
    };

    这个删除器首先调用Test对象的doSomething方法,然后使用delete来释放对象。这意味着,除了释放内存之外,你还可以执行一些清理工作,比如关闭文件、释放网络资源等。

    使用场景

    删除器的一些典型使用场景包括:

    • 资源管理:当你需要对某些特定资源执行特定的释放策略时,比如使用特定的库函数来释放资源。
    • 调试和日志记录:在释放资源前进行日志记录,帮助调试。
    • 错误处理和资源回收:在释放资源前检查错误状态或进行必要的资源回收。

    创建带删除器的unique_ptr

    你可以这样创建一个带有自定义删除器的unique_ptr

    1
    unique_ptr<Test, DestructTest> t6(new Test);

    这行代码创建了一个unique_ptr,它管理一个Test对象,并且当unique_ptr被销毁或重新赋值时,将使用DestructTest删除器来释放Test对象。这提供了一种灵活的方式来控制资源的释放过程。

    总结

    自定义删除器为智能指针提供了额外的灵活性,使得资源管理更加安全和灵活。通过使用删除器,你可以确保即使在需要特殊处理的场景下,资源也能被正确管理。

  2. 赋值

    1
    2
    3
    4
    unique_ptr<Test> t7(new Test);
    unique_ptr<Test> t8(new Test);
    t7 = std::move(t8); // 必须使用移动语义,结果,t7的内存释放,t8的内存交给t7管理
    t7->doSomething();
  3. 主动释放对象

    1
    2
    3
    4
    unique_ptr<Test> t9(new Test);
    // 法一:t9 = NULL;
    // 法二:t9 = nullptr;
    // 法三:t9.reset();
  4. 放弃对象的控制权

    1
    Test *t10 = t9.release();
  5. 重置

    1
    t9.reset(new Test);

  • auto_ptrunique_ptr智能指针的内存管理陷阱
1
2
3
4
5
6
7
8
9
10
auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str); // p1托管str指针
{
auto_ptr<string> p2;
p2.reset(str); // p2接管str指针时,会先取消p1的托管,然后再对str的托管
}

// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;

这是由于auto_ptrunique_ptr的排他性所导致的!
为了解决这样的问题,我们可以使用shared_ptr指针指针!





3.4 share_ptr

熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?

如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
public:
Person(int v) {
this->no = v;
cout << "构造函数 \t no = " << this->no << endl;
}

~Person() {
cout << "析构函数 \t no = " << this->no << endl;
}

private:
int no;
};

// 仿函数,内存删除
class DestructPerson {
public:
void operator() (Person *pt) {
cout << "DestructPerson..." << endl;
delete pt;
}
};
  1. 引用计数的使用

    调用use_count函数可以获得当前托管指针的引用计数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    shared_ptr<Person> sp1;

    shared_ptr<Person> sp2(new Person(2));

    // 获取智能指针管控的共享指针的数量 use_count():引用计数
    cout << "sp1 use_count() = " << sp1.use_count() << endl;
    cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;

    // 共享
    sp1 = sp2;

    cout << "sp1 use_count() = " << sp1.use_count() << endl;
    cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;

    shared_ptr<Person> sp3(sp1);
    cout << "sp1 use_count() = " << sp1.use_count() << endl;
    cout << "sp2 use_count() = " << sp2.use_count() << endl;
    cout << "sp2 use_count() = " << sp3.use_count() << endl << endl;

    如上代码,sp1 = sp2;shared_ptr< Person > sp3(sp1);就是在使用引用计数了。

    sp1 = sp2;sp1sp2共同托管同一个指针,所以他们的引用计数为2;

    shared_ptr< Person > sp3(sp1);sp1sp2sp3共同托管同一个指针,所以他们的引用计数为3;

  2. 构造

    1). shared_ptr< T > sp1; 空的shared_ptr,可以指向类型为T的对象

    1
    2
    3
    shared_ptr<Person> sp1;
    Person *person1 = new Person(1);
    sp1.reset(person1); // 托管person1

    2). shared_ptr< T > sp2(new T()); 定义shared_ptr,同时指向类型为T的对象

    1
    2
    shared_ptr<Person> sp2(new Person(2));
    shared_ptr<Person> sp3(sp1);

    3). shared_ptr<T[]> sp4; 空的shared_ptr,可以指向类型为T[]的数组对象,C++17后支持

    1
    shared_ptr<Person[]> sp4;

    4). shared_ptr<T[]> sp5(new T[] { … }); 指向类型为T的数组对象,C++17后支持

    1
    shared_ptr<Person[]> sp5(new Person[5] { 3, 4, 5, 6, 7 });

    5). shared_ptr< T > sp6(NULL, D()); 空的shared_ptr,接受一个D类型的删除器,使用D释放内存

    1
    shared_ptr<Person> sp6(NULL, DestructPerson());

    6). shared_ptr< T > sp7(new T(), D()); 定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存

    1
    shared_ptr<Person> sp7(new Person(8), DestructPerson());
  3. 初始化

    1). 方式一:构造函数

    1
    2
    shared_ptr<int> up1(new int(10));  // int(10) 的引用计数为1
    shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2

    2). 方式二:使用make_shared 初始化对象,分配内存效率更高(推荐使用)
    make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;

    用法:
    make_shared<类型>(构造类型对象需要的参数列表);

    1
    2
    3
    shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
    shared_ptr<string> up4 = make_shared<string>("字符串");
    shared_ptr<Person> up5 = make_shared<Person>(9);
  4. 赋值

    1
    2
    3
    shared_ptrr<int> up1(new int(10));  // int(10) 的引用计数为1
    shared_ptr<int> up2(new int(11)); // int(11) 的引用计数为1
    up1 = up2; // int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
  5. 主动释放对象

    1
    2
    3
    4
    shared_ptrr<int> up1(new int(10));
    // up1 = nullptr ; // int(10) 的引用计数减1,计数归零内存释放
    // 或
    // up1 = NULL; // 作用同上
  6. 重置

    首先p1是一个指针!

    p.reset() ; 将p重置为空指针,所管理对象引用计数减1
    p.reset(p1);将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
    p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器

  7. 交换

    p1p2 是智能指针

    1
    2
    std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
    p1.swap(p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变

  • shared_ptr使用陷阱

shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!

如下代码:
Boy类中有Girl的智能指针;
Girl类中有Boy的智能指针;
当他们交叉互相持有对方的管理对象时…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <string>
#include <memory>

using namespace std;

class Girl;

class Boy {
public:
Boy() {
cout << "Boy 构造函数" << endl;
}

~Boy() {
cout << "~Boy 析构函数" << endl;
}

void setGirlFriend(shared_ptr<Girl> _girlFriend) {
this->girlFriend = _girlFriend;
}

private:
shared_ptr<Girl> girlFriend;
};

class Girl {
public:
Girl() {
cout << "Girl 构造函数" << endl;
}

~Girl() {
cout << "~Girl 析构函数" << endl;
}

void setBoyFriend(shared_ptr<Boy> _boyFriend) {
this->boyFriend = _boyFriend;
}

private:
shared_ptr<Boy> boyFriend;
};


void useTrap() {
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());

// 陷阱用法
spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
// 此时boy和girl的引用计数都是2
}


int main(void) {
useTrap();

system("pause");
return 0;
}

运行截图:

循环引用问题

在这个例子中,BoyGirl类的对象互相持有对方的shared_ptr

  • spBoy->setGirlFriend(spGirl);被调用时,Boy对象持有了一个指向Girl对象的shared_ptr,这使得Girl对象的引用计数增加到2。
  • spGirl->setBoyFriend(spBoy);被调用时,Girl对象持有了一个指向Boy对象的shared_ptr,这使得Boy对象的引用计数增加到2。

此时,每个对象都通过shared_ptr被另一个对象持有,形成了一个循环引用。即使在useTrap()函数执行完毕后,spBoyspGirl的生命周期结束,它们的引用计数减少了1,变成了1,但由于互相持有,它们的引用计数永远不会达到0,导致BoyGirl对象都不会被析构和释放。

所以在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!

当然,这也是有办法解决的,那就是使用weak_ptr弱指针。

针对上面的情况,还讲一下另一种情况。如果是单方获得管理对方的共享指针,那么这样着是可以正常释放掉的!
例如:

1
2
3
4
5
6
7
8
void useTrap() {
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());

// 单方获得管理
//spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
}

反过来也是一样的!

这是什么原因呢?

  1. 首先释放spBoy,但是因为girl对象里面的智能指针还托管着boy,boy的引用计数为2,所以释放spBoy时,引用计数减1,boy的引用计数为1;
  2. 在释放spGirl,girl的引用计数减1,为零,开始释放girl的内存,因为girl里面还包含有托管boy的智能指针对象,所以也会进行boyFriend的内存释放,boy的引用计数减1,为零,接着开始释放boy的内存。最终所有的内存都释放了。




3.5 weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*->但可以使用 lock 获得一个可用的 shared_ptr 对象。

  1. 弱指针的使用;

    1
    2
    3
    weak_ptr wpGirl_1;         // 定义空的弱指针
    weak_ptr wpGirl_2(spGirl); // 使用共享指针构造
    wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针
  2. 弱指针也可以获得引用计数;

    1
    wpGirl_1.use_count();
  3. 弱指针不支持 *-> 对指针的访问;

  4. 在必要的使用可以转换成共享指针 lock()

    1
    2
    3
    4
    5
    shared_ptr<Girl> sp_girl;
    sp_girl = wpGirl_1.lock();

    // 使用完之后,再将共享指针置NULL即可
    sp_girl = NULL;

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());

// 弱指针的使用
weak_ptr<Girl> wpGirl_1; // 定义空的弱指针
weak_ptr<Girl> wpGirl_2(spGirl); // 使用共享指针构造
wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针

cout << "spGirl \t use_count = " << spGirl.use_count() << endl;
cout << "wpGirl_1 \t use_count = " << wpGirl_1.use_count() << endl;


// 弱指针不支持 * 和 -> 对指针的访问
/*wpGirl_1->setBoyFriend(spBoy);
(*wpGirl_1).setBoyFriend(spBoy);*/

// 在必要的使用可以转换成共享指针
shared_ptr<Girl> sp_girl;
sp_girl = wpGirl_1.lock();

cout << sp_girl.use_count() << endl;
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;

当然这只是一些使用上的小例子,具体用法如下:

解决循环引用问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>
#include <string>
#include <memory>

using namespace std;

class Girl;

class Boy {
public:
Boy() {
cout << "Boy 构造函数" << endl;
}

~Boy() {
cout << "~Boy 析构函数" << endl;
}

void setGirlFriend(shared_ptr<Girl> _girlFriend) {
this->girlFriend = _girlFriend;


// 在必要的使用可以转换成共享指针
shared_ptr<Girl> sp_girl;
sp_girl = this->girlFriend.lock();

cout << sp_girl.use_count() << endl;
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;
}

private:
weak_ptr<Girl> girlFriend;
};

class Girl {
public:
Girl() {
cout << "Girl 构造函数" << endl;
}

~Girl() {
cout << "~Girl 析构函数" << endl;
}

void setBoyFriend(shared_ptr<Boy> _boyFriend) {
this->boyFriend = _boyFriend;
}

private:
shared_ptr<Boy> boyFriend;
};


void useTrap() {
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());

spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
}


int main(void) {
useTrap();

system("pause");
return 0;
}

在类中使用弱指针接管共享指针,在需要使用时就转换成共享指针去使用即可!

自此问题完美解决!


  • expired函数的用法

expired:判断当前weak_ptr智能指针是否还有托管的对象,有则返回false,无则返回true

如果返回true,等价于 use_count() == 0,即已经没有托管的对象了;当然,可能还有析构函数进行释放内存,但此对象的析构已经临近(或可能已发生)。

示例
演示如何用 expired 检查指针的合法性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <memory>

std::weak_ptr<int> gw;

void f() {

// expired:判断当前智能指针是否还有托管的对象,有则返回false,无则返回true
if (!gw.expired()) {
std::cout << "gw is valid\n"; // 有效的,还有托管的指针
}
else {
std::cout << "gw is expired\n"; // 过期的,没有托管的指针
}
}

int main() {
{
auto sp = std::make_shared<int>(42);
gw = sp;

f();
}

// 当{ }体中的指针生命周期结束后,再来判断其是否还有托管的指针
f();

return 0;
}

在 { } 中,sp的生命周期还在,gw还在托管着make_shared赋值的指针(sp),所以调用f()函数时打印"gw is valid\n";
当执行完 { } 后,sp的生命周期已经结束,已经调用析构函数释放make_shared指针内存(sp),gw已经没有在托管任何指针了,调用expired()函数返回true,所以打印"gw is expired\n";





3.6 智能指针的使用陷阱

  1. 不要把一个原生指针给多个智能指针管理;

    1
    2
    3
    4
    5
    6
    7
    int *x = new int(10);
    unique_ptr< int > up1(x);
    unique_ptr< int > up2(x);
    // 警告! 以上代码使up1 up2指向同一个内存,非常危险
    // 或以下形式:
    up1.reset(x);
    up2.reset(x);
    查看解释

    示例代码展示了std::unique_ptr的一个非常关键的错误使用方式,即两个std::unique_ptr实例被设置为指向同一个动态分配的内存。这是非常危险的,因为std::unique_ptr被设计为拥有其指向对象的独占所有权,意味着当一个std::unique_ptr对象被销毁时,它会自动删除其管理的内存。如果有两个std::unique_ptr指向同一个内存地址,当其中一个std::unique_ptr被销毁(或者其reset方法被调用)时,它会删除内存,而另一个std::unique_ptr仍然认为它拥有那块内存的所有权,这将在其析构或reset时再次尝试删除同一块内存,导致未定义行为,最常见的是程序崩溃。

    结论

    永远不要让两个unique_ptr指向同一块内存。这种做法违背了unique_ptr的设计原则,并且会导致运行时错误。正确的资源管理和智能指针的使用是现代C++中非常重要的一部分,需要仔细设计以避免这类问题。

  2. 记得使用u.release()的返回值;
    在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.

  3. 禁止delete 智能指针 get 函数返回的指针;
    如果我们主动释放掉 get 函数获得的指针,那么智能指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!

  4. 禁止用任何类型智能指针 get 函数返回的指针去初始化另外一个智能指针!

    1
    2
    shared_ptr< int > sp1(new int(10));
    // 一个典型的错误用法 shared_ptr< int > sp4(sp1.get());

总结:

智能指针虽然使用起来很方便,但是要注意使用智能指针的一些陷阱,否则会造成严重的内存报错或者内存泄露等问题!





4. 引用

C++中的引用本质上是对另一个变量的别名,它提供了另一种访问变量的方式。从实现的角度看,引用通常是通过指针来实现的,但这对于C++程序员是透明的,即编程时你不需要也不应该将引用视为指针。

实现细节:

  • 编译器处理:编译器在处理引用时,内部会将引用视为指向被引用变量的指针。当你通过引用访问变量时,编译器生成的代码实际上是通过这个指针来访问变量的。
  • 语义层面的差异:尽管在实现层面引用可能通过指针实现,但引用和指针在语义上有明显区别。引用在定义时必须被初始化,并且不能被重新绑定到另一个变量;而指针可以在任何时候被赋予新的地址值。

编译阶段的处理

  • 引用的创建:在编译阶段,当声明一个引用时,编译器确保引用被正确地初始化为某个变量的别名。这可能涉及到调整代码,以确保通过引用的访问被转换为对实际变量的访问。
  • 引用的使用:当通过引用读写变量时,编译器生成的代码并不是对引用本身的操作,而是直接对被引用的变量进行操作。这意味着,尽管引用的实现可能依赖于指针,但使用引用的代码与直接操作变量的代码在效果上是一致的,没有额外的间接层或性能开销。

举例说明:

假设有如下代码:

1
2
3
int x = 10;
int& ref = x;
ref = 20;

在这段代码中,ref是变量x的引用。从程序员的视角看,ref就是x的另一个名字。在编译阶段,编译器处理对ref的赋值操作,就如同直接对x赋值一样,确保x的值被更新为20。实际生成的机器码将直接反映这一操作,而不会表现出任何间接引用的痕迹。

结论:

虽然C++标准没有规定引用必须通过指针来实现,但在很多编译器的实现中,引用的行为是通过指针实现的。关键的区别在于,引用提供的是一种更安全、更直观的语法来间接操作变量,同时在语义上隐藏了指针的复杂性。引用的引入,使得C++支持更丰富的编程模式,如函数参数传递、操作符重载等,同时保持了代码的简洁性和效率。



  • 引用做函数返回值

作用:引用是可以作为函数的返回值存在的

注意:不要返回局部变量引用

用法:函数调用作为左值

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

using namespace std;

//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}

//返回静态变量引用
int& test02() {
static int a = 20;
return a;
}

int main() {

//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl; // 第一次结果正确,是因为编译器做了保留
cout << "ref = " << ref << endl; // 第二次结果错误,是因为a的内存已经释放

//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;

test02() = 1000;

cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;

system("pause");

return 0;
}

查看输出
1
2
3
4
5
6
ref = 10
ref = 2077998048
ref2 = 20
ref2 = 20
ref2 = 1000
ref2 = 1000


  • 常量引用

作用: 常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

using namespace std;

// 引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
// v += 10;
cout << v << endl;
}

int main() {

// int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
// 加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;

// ref = 100; //加入const后不可以修改变量
cout << ref << endl;

// 函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);

return 0;
}

GPT4:

在C++中,尝试将一个非const引用绑定到临时对象(如字面量10)是不合法的,因此,int &a = 10;这条语句会导致编译错误。非const引用需要绑定到一个可修改的左值(lvalue),而字面量10是一个右值(rvalue),意味着它是一个临时的值,不可以被修改。

相反,const int &a = 10;是合法的,因为const引用可以绑定到临时对象。在这种情况下,编译器会为临时值10创建一个匿名的、不可修改的临时变量,并让a引用它。这是一个允许const引用绑定到右值的特例,主要用途是允许函数参数以const引用的方式接受临时对象,以提高效率和灵活性。

示例解释

  • 不合法的示例
1
int &a = 10; // 错误: 非const引用不能绑定到临时对象

这个示例试图将一个非const引用a绑定到字面量10上。因为字面量10是一个临时的右值,不能被修改,所以这是不合法的。

  • 合法的示例
1
const int &a = 10; // 合法: const引用可以绑定到临时对象

这里,const int &a = 10;声明了一个const引用a,并将其绑定到字面量10上。尽管10是一个临时右值,但由于aconst引用,所以这是允许的。这种方式常用于函数参数传递,使得函数可以接受临时对象作为输入,同时避免了复制的开销。

结论

在C++中,非const引用不能绑定到临时对象上,而const引用可以。这反映了C++设计中的一个基本原则:通过限制对数据的修改来增加代码的安全性和灵活性。在实践中,这意味着你应该使用const引用来传递那些不需要修改的临时对象,以提高效率和代码的可读性。





5. 函数

  • 函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法: 返回值类型 函数名 (参数= 默认值){}

规则

  1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
  2. 如果函数声明有默认值,函数实现的时候就不能有默认参数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;


int func1(int a, int b = 10, int c = 10) {
return a + b + c;
}

int func2(int a = 10, int b = 10);
int func2(int a, int b) {
return a + b;
}

int main() {

cout << "ret = " << func1(20, 20) << endl;
cout << "ret = " << func2(100) << endl;

system("pause");

return 0;
}

√:

1
2
3
4
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
return a + b;
}
1
2
3
4
int func2(int a, int b);
int func2(int a = 10, int b = 10) {
return a + b;
}

×:(报错为:重定义默认参数)

1
2
3
4
int func2(int a = 10, int b = 10);
int func2(int a = 20, int b = 30) {
return a + b;
}


  • 占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型){}

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数占位参数 ,占位参数也可以有默认参数
// void func(int a, int = 10) {
void func(int a, int) {
cout << "this is func" << endl;
}

int main() {

func(10,10); //占位参数必须填补

system("pause");

return 0;
}


  • 函数重载

作用:函数名可以相同,提高复用性

函数重载满足条件

  • 同一个作用域下
  • 函数名称相同
  • 函数参数 类型不同 或者 个数不同 或者 顺序不同

注意: 函数的返回值不可以作为函数重载的条件

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>

using namespace std;


//函数重载需要函数都在同一个作用域下
void func()
{
cout << "func 的调用!" << endl;
}
void func(int a)
{
cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
cout << "func (double a)的调用!" << endl;
}
void func(int a, double b)
{
cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a, int b)
{
cout << "func (double a ,int b)的调用!" << endl;
}

//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
// cout << "func (double a ,int b)的调用!" << endl;
//}


int main() {

func();
func(10);
func(3.14);
func(10, 3.14);
func(3.14, 10);

system("pause");

return 0;
}

  • 函数重载注意事项
  • 引用作为重载条件?
  • 函数重载碰到函数默认参数?

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

//函数重载注意事项
//1、引用作为重载条件

void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}

void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}


//2、函数重载碰到函数默认参数

void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}

int main() {

int a = 10;
func(a); //调用无const
func(10);//调用有const


//func2(10); //碰到默认参数产生歧义,需要避免

system("pause");

return 0;
}




6. 类和对象

C++面向对象的三大特性为:封装、继承、多态

C++认为万事万物都皆为对象,对象上有其属性和行为

例如:

人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…

车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…

具有相同性质的对象,我们可以抽象称为,人属于人类,车属于车类


6.1 封装

封装是C++面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制


  • 封装的意义一

在设计类的时候,属性和行为写在一起,表现事物

语法: class 类名{ 访问权限: 属性 / 行为 };

示例1: 设计一个圆类,求圆的周长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

// 圆周率
const double PI = 3.14;

// 1、封装的意义
// 将属性和行为作为一个整体,用来表现生活中的事物

// 封装一个圆类,求圆的周长
// class代表设计一个类,后面跟着的是类名
class Circle
{
public: // 访问权限 公共的权限

// 属性
int m_r;// 半径

// 行为
// 获取到圆的周长
double calculateZC()
{
// 2 * pi * r
// 获取圆的周长
return 2 * PI * m_r;
}
};

int main() {

// 通过圆类,创建圆的对象
// c1就是一个具体的圆
Circle c1;
c1.m_r = 10; // 给圆对象的半径 进行赋值操作

// 2 * pi * 10 = = 62.8
cout << "圆的周长为: " << c1.calculateZC() << endl;

system("pause");

return 0;
}


  • 封装的意义二

C++中类的访问权限控制是面向对象编程的核心特性之一,它帮助封装和隐藏了类的内部实现,只暴露必要的接口给类的使用者。C++提供了三种访问权限修饰符:publicprotectedprivate,它们定义了类成员(包括数据成员和成员函数)的访问范围。

public

  • 含义:被public修饰的成员可以在任何地方被访问,无论是类的内部还是外部。
  • 用途:通常用来定义类的接口,即那些可以安全地被类的使用者直接访问和使用的成员。

protected

  • 含义:被protected修饰的成员在类内部和派生类(子类)中可以被访问,但不能被类的实例直接访问。
  • 用途protected成员主要用于继承关系中,允许子类访问和修改这些被保护的成员,同时对外隐藏实现细节。

private

  • 含义:被private修饰的成员只能被类的成员函数和友元函数(通过friend声明)访问,对类的使用者是不可见的。
  • 用途private成员用于实现类的内部逻辑,是类的实现细节,它们对类的使用者是隐藏的。

默认访问权限

  • :如果不显式指定访问权限,则类的成员默认是private 的。
  • 结构体(struct):结构体的成员默认是public 的。这是结构体和类的一个主要区别,尽管它们在C++中都支持成员函数和继承等面向对象的特性。

访问权限示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyClass {
public:
int publicVar; // 可以在任何地方访问
void publicMethod() {} // 可以在任何地方访问

protected:
int protectedVar; // 只能在类内部、友元和派生类中访问
void protectedMethod() {} // 只能在类内部、友元和派生类中访问

private:
int privateVar; // 只能在类内部和友元中访问
void privateMethod() {} // 只能在类内部和友元中访问
};

class DerivedClass : public MyClass {
void foo() {
publicVar = 1; // OK
protectedVar = 2; // OK
// privateVar = 3; // 错误:privateVar在派生类中不可访问
}
};

int main() {
MyClass obj;
obj.publicVar = 1; // OK
// obj.protectedVar = 2; // 错误:protectedVar在类外部不可访问
// obj.privateVar = 3; // 错误:privateVar在类外部不可访问
}

通过正确使用访问权限修饰符,可以确保类的内部状态被适当地保护,同时向类的使用者提供一个清晰、稳定的接口。这有助于提高代码的可维护性和可扩展性。



  • 成员属性设置为私有

优点1: 将所有成员属性设置为私有,可以自己控制读写权限

优点2: 对于写权限,我们可以检测数据的有效性

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <string>

using namespace std;

class Person {
public:

//姓名设置可读可写
void setName(string name) {
m_Name = name;
}
string getName()
{
return m_Name;
}


//获取年龄
int getAge() {
return m_Age;
}
//设置年龄
void setAge(int age) {
if (age < 0 || age > 150) {
cout << "???" << endl;
return;
}
m_Age = age;
}

//Idol设置为只写
void setIdol(string Idol) {
m_Idol = Idol;
}

private:
string m_Name; //可读可写 姓名

int m_Age; //只读 年龄

string m_Idol; //只写 Idol
};


int main() {

Person p;
//姓名设置
p.setName("张三");
cout << "姓名: " << p.getName() << endl;

//年龄设置
p.setAge(50);
cout << "年龄: " << p.getAge() << endl;

//Idol设置
p.setIdol("马嘉祺");
//cout << "Idol: " << p.m_Idol << endl; //只写属性,不可以读取

system("pause");

return 0;
}


  • 类的声明和定义分离

在实际的C++项目中,将类的声明和定义分别放在不同的文件中是一种常见且推荐的做法。这种做法不仅有助于代码的组织和管理,还能提高编译效率。具体来说,类的声明通常放在头文件(.h.hpp文件)中,而类成员函数的定义则放在源文件(.cpp文件)中。

类的声明:

类的声明提供了关于类的基本信息,包括类的名称、它包含的数据成员(属性)以及成员函数(方法)的原型。类的声明基本上是类的一个接口说明,告诉使用者可以如何与类的对象进行交互,而无需了解具体的实现细节。

示例:MyClass.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
public:
MyClass(); // 构造函数
~MyClass(); // 析构函数
void myMethod(); // 一个成员函数
private:
int myDataMember; // 一个数据成员
};

#endif

类的定义:

类的定义包含了数据成员和成员函数的具体实现。将定义放在源文件中,可以隐藏实现细节,同时允许在不影响使用类的代码的情况下更改这些实现。此外,当类定义发生变化时,只需重新编译实现该类的源文件,而不是所有包含了类声明的头文件的源文件,这样可以减少编译依赖,提高编译效率。

示例:MyClass.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
// MyClass.cpp
#include "MyClass.h"

MyClass::MyClass() : myDataMember(0) { // 构造函数的实现
}

MyClass::~MyClass() { // 析构函数的实现
// 清理资源
}

void MyClass::myMethod() { // 成员函数的实现
// 方法实现
}

使用类:

当其他源文件需要使用MyClass时,只需包含类的头文件。这样,编译器在编译这些源文件时就能够识别MyClass的存在和它的接口,而无需知道类成员函数的具体实现。

示例:main.cpp

1
2
3
4
5
6
7
#include "MyClass.h"

int main() {
MyClass obj; // 创建MyClass的对象
obj.myMethod(); // 调用成员函数
return 0;
}

总结:

将类的声明和定义分别放在头文件和源文件中是C++项目中的一种最佳实践。这种做法有助于提高代码的可读性和可维护性,同时减少不必要的编译依赖,提高编译效率。





6.2 构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

一个对象或者变量没有初始状态,对其使用后果是未知

同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

c++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供

编译器提供的构造函数和析构函数是空实现。

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

using namespace std;

class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}

};

void test01()
{
Person p;
}

int main() {

test01();

system("pause");

return 0;
}
用析构函数来释放内存

当一个类的对象被销毁时,C++不会自动释放对象中指针成员所指向的动态分配的内存。这是因为C++运行时系统不会跟踪指针所指向的内存;管理这块内存的责任在于开发者。如果你在类中使用了指向动态分配内存的指针,你需要在类的析构函数中显式地释放这块内存,以避免内存泄漏。

为什么需要显式释放内存

C++提供了自动内存管理的机制,如栈上的自动变量,其生命周期由编译器自动管理。然而,对于通过new操作符在堆上动态分配的内存,C++要求程序员显式地调用delete来释放内存。这种设计给予了程序员更大的灵活性和控制权,但也带来了更大的责任,特别是在异常安全和资源管理方面。

示例:使用析构函数释放内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
public:
int* data;

// 构造函数动态分配内存
MyClass(int value) {
data = new int(value);
}

// 析构函数释放内存
~MyClass() {
delete data;
}
};

在这个示例中,MyClass的构造函数中分配了一个整数的动态内存,析构函数中释放了这块内存。当MyClass的对象离开其作用域被销毁时,析构函数会自动被调用,从而释放动态分配的内存。

内存管理的最佳实践

为了避免内存泄漏和其他资源管理错误,C++11引入了智能指针(如std::unique_ptrstd::shared_ptr),它们可以自动管理动态分配的内存。使用智能指针,你可以避免直接使用原始指针带来的大多数内存管理问题:

1
2
3
4
5
6
7
8
#include <memory>

class MyClass {
public:
std::unique_ptr<int> data;

MyClass(int value) : data(new int(value)) {}
};

在这个示例中,std::unique_ptr接管了动态内存的生命周期管理。当MyClass的对象被销毁时,std::unique_ptr的析构函数会自动释放其所管理的内存,无需手动编写析构代码来释放内存。

总之,虽然C++不会自动释放对象中指针所指向的内存,但通过合理使用析构函数和智能指针,可以有效管理动态分配的资源,避免内存泄漏。



  • 构造函数的分类及调用

两种分类方式:

  • 按参数分为: 有参构造和无参构造

  • 按类型分为: 普通构造和拷贝构造

三种调用方式:

  • 括号法

  • 显示法

  • 隐式转换法

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#include <iostream>

using namespace std;

//1、构造函数分类
// 按照参数分类分为 有参和无参构造 无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person& p) {
age = p.age;#include <iostream>

using namespace std;

//1、构造函数分类
// 按照参数分类分为 有参和无参构造 无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

//2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();

//2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构

//2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);

//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//Person(p4); // 编译器认为是Person p4; 然后发生重定义错误
}

int main() {

test01();
//test02();

system("pause");

return 0;
}
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

//2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();

//2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构

//2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);

//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//Person p5(p4);
}

int main() {

test01();
//test02();

system("pause");

return 0;
}
构造函数一定是公有的吗?

构造函数不一定非要设置为公有(public)。将构造函数设置为公有是最常见的情况,因为这允许在类的外部创建对象实例。然而,在某些特定的设计场景中,将构造函数设置为受保护(protected)或私有(private)也是有意义的。这样做通常与设计模式相关,比如单例模式、工厂模式或者是当你想限制对象创建方式时。

私有构造函数

当你将构造函数设置为私有时,这意味着不能在类的外部直接实例化该类。这种技术常用于实现如单例模式这样的设计模式。

单例模式示例

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 保证这是一个单例
return instance;
}
private:
Singleton() {} // 私有构造函数
// 禁用拷贝构造和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};

在这个例子中,Singleton类的构造函数是私有的,这防止了类的外部直接创建Singleton类的实例。相反,实例通过getInstance静态成员函数访问,确保类只有一个实例。

受保护的构造函数

将构造函数设置为受保护的,这意味着只有该类及其派生类能够调用这个构造函数。

受保护构造函数示例

1
2
3
4
5
6
7
8
9
class Base {
protected:
Base() {} // 受保护构造函数
};

class Derived : public Base {
public:
Derived() : Base() {} // 派生类可以访问受保护的构造函数
};

这种情况下,只有Base类的派生类可以创建Base类的实例,这可以用在当你想要限制类的实例化,使之只能作为基类被继承时。

总结

构造函数不必总是公有的。根据类的设计目的,将构造函数设为私有或受保护的也是合理且有用的设计决策。通过限制构造函数的访问级别,你可以更精细地控制对象的创建和类的使用方式,以满足特定的设计需求。

如果没有显式定义拷贝构造函数,那是如何拷贝的?

拷贝构造函数是一种特殊的构造函数,其作用是初始化一个新对象作为现有对象的副本。它确实可以看作是一种能够接受同类对象作为参数的重载构造函数,但它有特定的形式和用途。拷贝构造函数的典型声明如下:

1
ClassName(const ClassName& other);

这里的ClassName代表类名,other是对另一个同类型对象的引用,该引用是常量,意味着拷贝构造函数不应修改被拷贝的对象。

拷贝构造函数的特点

  1. 参数:拷贝构造函数的参数是当前类类型的一个常量引用。它不接受其他类型的参数。
  2. 作用:用于在以下几种情况下创建一个对象的副本:
    • 显式或隐式地将一个对象作为参数传递给函数。
    • 从函数返回一个对象。
    • 用一个对象初始化另一个对象。
  3. 默认行为:如果没有为类显式定义拷贝构造函数,C++编译器会自动生成一个默认的拷贝构造函数,执行成员逐个拷贝(通常是浅拷贝)。
  4. 深拷贝 vs 浅拷贝:如果类成员包括指向动态分配内存的指针,通常需要显式定义拷贝构造函数来实现深拷贝,以避免潜在的内存管理问题。

如果你没有为你的类显式定义一个拷贝构造函数,C++编译器会为你自动生成一个默认的拷贝构造函数。这个自动生成的拷贝构造函数执行的是浅拷贝(shallow copy),意味着它会逐个复制对象的所有成员变量到新对象中,这个复制过程是按成员的位复制(bitwise copy)进行的。

浅拷贝的含义和影响:

  • 对于非指针类型的成员,浅拷贝简单地复制其值。这对于内置数据类型(如intfloat等)和包含这些类型的复合数据类型通常是足够的。
  • 对于指针类型的成员,浅拷贝只复制指针的值,而不复制指针所指向的数据。因此,原始对象和拷贝对象的指针成员将指向同一个内存地址。这可能会导致问题,如:
    • 当原始对象或拷贝对象被销毁时,它们的析构函数可能尝试释放同一个内存地址,造成双重释放问题(double-free)。
    • 修改一个对象中的动态分配数据也会影响另一个对象,因为它们共享同一个数据。

深拷贝:

为了解决浅拷贝可能导致的问题,特别是在类成员包含指向动态分配内存的指针时,可以显式定义自己的拷贝构造函数来实现深拷贝(deep copy)。深拷贝不仅复制指针的值,还复制指针所指向的数据到新的内存地址,这样原始对象和拷贝对象就有了各自独立的数据副本,互不干扰。

示例:

下面是一个演示默认拷贝构造函数只执行浅拷贝的例子:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
int* data;
MyClass(int value) { data = new int(value); }
~MyClass() { delete data; }
};

int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 使用默认拷贝构造函数,浅拷贝
// 此时obj1.data和obj2.data指向同一块内存
}

如果MyClass需要深拷贝行为,你应该显式定义一个拷贝构造函数来正确复制data成员指向的内存。

总结:

当类中没有显式定义拷贝构造函数时,C++编译器生成的默认拷贝构造函数执行浅拷贝。这在许多情况下是合理的,但如果类成员包含指针指向动态分配的内存,就需要考虑实现深拷贝以避免潜在的内存管理问题。



  • 拷贝构造函数的调用时机

C++中拷贝构造函数调用时机通常有三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>

using namespace std;

class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Person() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {

Person man(100); //p对象已经创建完毕
Person newman(man); //调用拷贝构造函数
Person newman2 = man; //拷贝构造

//Person newman3;
//newman3 = man; //不是调用拷贝构造函数,赋值操作
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
Person p; //无参构造函数
doWork(p);
}

//3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}

void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}


int main() {

//test01();
//test02();
test03();

system("pause");

return 0;
}


  • 构造函数调用规则

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数


  • 初始化列表

作用:

C++提供了初始化列表语法,用来初始化属性

语法: 构造函数():属性1(值1),属性2(值2)... {}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
public:

////传统方式初始化
//Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
//}

//初始化列表方式初始化
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
void PrintPerson() {
cout << "mA:" << m_A << endl;
cout << "mB:" << m_B << endl;
cout << "mC:" << m_C << endl;
}
private:
int m_A;
int m_B;
int m_C;
};


6.3 类的成员

  • 类对象作为类的成员

C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员

例如:

1
2
3
4
5
class A {}
class B
{
A a;
}

B类中有对象A作为成员,A为对象成员

那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <string>

using namespace std;

class Phone
{
public:
Phone(string name)
{
m_PhoneName = name;
cout << "Phone构造" << endl;
}

~Phone()
{
cout << "Phone析构" << endl;
}

string m_PhoneName;

};


class Person
{
public:

// 初始化列表可以告诉编译器调用哪一个构造函数
Person(string name, string pName) :m_Name(name), m_Phone(pName)
{
cout << "Person构造" << endl;
}

~Person()
{
cout << "Person析构" << endl;
}

void playGame()
{
cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
}

string m_Name;
Phone m_Phone;

};
void test01()
{
// 当类中成员是其他类对象时,我们称该成员为 对象成员
// 构造的顺序是 :先调用对象成员的构造,再调用本类构造
// 析构顺序与构造相反
Person p("张三", "苹果X");
p.playGame();

}


int main() {

test01();

system("pause");

return 0;
}


  • 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

示例1 :静态成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>

using namespace std;

class Person
{

public:

static int m_A; //静态成员变量

//静态成员变量特点:
//1 在编译阶段分配内存
//2 类内声明,类外初始化
//3 所有对象共享同一份数据

private:
static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;
int Person::m_B = 10;

void test01()
{
//静态成员变量两种访问方式

//1、通过对象
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl;

Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
cout << "p2.m_A = " << p2.m_A << endl;

//2、通过类名
cout << "m_A = " << Person::m_A << endl;


//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
}

int main() {

test01();

system("pause");

return 0;
}

示例2:静态成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>

using namespace std;

class Person
{

public:

//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量

static void func()
{
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; //错误,不可以访问非静态成员变量
}

static int m_A; //静态成员变量
int m_B; //
private:

//静态成员函数也是有访问权限的
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_A = 10;


void test01()
{
//静态成员变量两种访问方式

//1、通过对象
Person p1;
p1.func();

//2、通过类名
Person::func();


//Person::func2(); //私有权限访问不到
}

int main() {

test01();

system("pause");

return 0;
}


6.4 对象模型和this指针

  • 对象模型
C和C++的空结构体所占内存

C语言中的空结构体和C++中的空结构体在所占用的内存大小方面有所不同。在C语言中,空结构体的大小未明确规定,不同的编译器可能会有不同的处理方式;而在C++中,空结构体的大小为1字节。这是因为C++标准规定了每个对象必须有一个独一无二的地址,即使是空对象也不例外,因此空结构体至少占用1字节的空间,以确保它可以被唯一地标识。这个规定有助于C++支持面向对象的特性,如多态和继承。

结构体与类

在C++中,结构体(struct)可以被看作是一个默认访问权限为public的类。除了默认的访问权限不同(类的默认访问权限是private),结构体和类在C++中几乎是相同的,都支持成员函数、继承、多态等面向对象的特性。这意味着你可以使用结构体来定义带有数据成员和成员函数的复杂类型,就像使用类一样

在C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

using namespace std;

class Person {
public:
Person() {
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};

int main() {

cout << sizeof(Person) << endl;

system("pause");

return 0;
}


  • this指针概念

通过阅读刚才的内容,我们知道在C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:这一块代码是如何区分那个对象调用自己的呢?

c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

using namespace std;

class Person
{
public:

Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}

Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}

int age;
};

void test01()
{
Person p1(10);
cout << "p1.age = " << p1.age << endl;

Person p2(10);

// 链式编程
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl;
}

int main() {

test01();

system("pause");

return 0;
}


  • 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保证代码的健壮性

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

using namespace std;

// 空指针访问成员函数
class Person {
public:

void ShowClassName() {
cout << "我是Person类!" << endl;
}

void ShowPerson() {
if (this == nullptr) {
return;
}
cout << mAge << endl;
}

public:
int mAge;
};

void test01()
{
Person * p = nullptr;
p->ShowClassName(); // 空指针,可以调用成员函数
p->ShowPerson(); // 但是如果成员函数中用到了this指针,就不可以了
}

int main() {

test01();

system("pause");

return 0;
}


  • const修饰成员函数

常函数

  • 成员函数后加const后我们称为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>

using namespace std;

class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}

//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {
//const Type* const pointer;
//this = nullptr; //不能修改指针的指向 Person* const this;
//this->mA = 100; //但是this指针指向的对象的数据是可以修改的

//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}

void MyFunc() const {
//mA = 10000;
}

public:
int m_A;
mutable int m_B; //可修改 可变的
};


//const修饰对象 常对象
void test01() {

const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数

}

int main() {

test01();

system("pause");

return 0;
}




6.5 友元

在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类 访问另一个类中私有成员

友元的关键字为 friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

  • 全局函数做友元

在C++中,将全局函数声明为类的友元,以允许它访问类的私有和保护成员,并不要求这个声明一定要位于类定义的最上方。友元声明可以放在类定义的任何位置,只要它出现在类的访问控制区域内即可。重要的是确保友元声明在类内部,并且友元函数的定义可以在类的外部。这样,友元函数就能够访问类的所有成员,包括私有成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Building
{
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building);

public:

Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}


public:
string m_SittingRoom; //客厅

private:
string m_BedRoom; //卧室
};


void goodGay(Building * building)
{
cout << "好基友正在访问: " << building->m_SittingRoom << endl;
cout << "好基友正在访问: " << building->m_BedRoom << endl;
}


void test01()
{
Building b;
goodGay(&b);
}

int main(){

test01();

system("pause");
return 0;
}


  • 类做友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <string>

using namespace std;

class Building;
class goodGay
{
public:

goodGay();
void visit();

private:
Building *building;
};


class Building
{
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;

public:
Building();

public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};

Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}

goodGay::goodGay()
{
building = new Building;
}

void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
goodGay gg;
gg.visit();

}

int main() {

test01();

system("pause");
return 0;
}


  • 成员函数做友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>
#include <string>

using namespace std;

class Building;
class goodGay
{
public:

goodGay();
void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容
void visit2();

private:
Building *building;
};


class Building
{
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();

public:
Building();

public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};

Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}

goodGay::goodGay()
{
building = new Building;
}

void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void goodGay::visit2()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
//cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
goodGay gg;
gg.visit();

}

int main() {

test01();

system("pause");
return 0;
}




6.6 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型


  • 加号运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <string>

using namespace std;

class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}


public:
int m_A;
int m_B;
};

//全局函数实现 + 号运算符重载
//Person operator+(const Person& p1, const Person& p2) {
// Person temp(0, 0);
// temp.m_A = p1.m_A + p2.m_A;
// temp.m_B = p1.m_B + p2.m_B;
// return temp;
//}

//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}

void test() {

Person p1(10, 10);
Person p2(20, 20);

//成员函数方式
Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;


Person p4 = p3 + 10; //相当于 operator+(p3,10)
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;

}

int main() {

test();

system("pause");

return 0;
}

总结1:对于内置的数据类型的表达式的的运算符是不可能改变的

总结2:不要滥用运算符重载(重载加号,就做好加号的事,别弄成乘啊除啊)



  • 左移运算符重载

作用:可以输出自定义数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <string>

using namespace std;

class Person {
friend ostream& operator<<(ostream& out, Person& p);

public:

Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}

//成员函数 实现不了 p << cout 不是我们想要的效果
//void operator<<(Person& p){
//}

private:
int m_A;
int m_B;
};

//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}

void test() {

Person p1(10, 20);

cout << p1 << "hello world" << endl; //链式编程
}

int main() {

test();

system("pause");

return 0;
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型



  • 递增运算符重载

作用: 通过重载递增运算符,实现自己的整型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<bits/stdc++.h>

using namespace std;

class MyInteger {

friend ostream& operator<<(ostream& out, MyInteger myint);

public:
MyInteger() {
m_Num = 0;
}
//前置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}

//后置++
//int代表占位参数,可以用于区分前置和后置递增
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}

private:
int m_Num;
};


ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}


//前置++ 先++ 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}

//后置++ 先返回 再++
void test02() {

MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}

int main() {

test01();
//test02();

system("pause");

return 0;
}

总结: 前置递增返回引用,后置递增返回值



  • 赋值运算符重载

c++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=, 对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include<bits/stdc++.h>

using namespace std;

class Person
{
public:

Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}

//重载赋值运算符
Person& operator=(Person &p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
//编译器提供的代码是浅拷贝
//m_Age = p.m_Age;

//提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);

//返回自身
return *this;
}


~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}

//年龄的指针
int *m_Age;

};


void test01()
{
Person p1(18);

Person p2(20);

Person p3(30);

p3 = p2 = p1; //赋值操作

cout << "p1的年龄为:" << *p1.m_Age << endl;

cout << "p2的年龄为:" << *p2.m_Age << endl;

cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main() {

test01();

//int a = 10;
//int b = 20;
//int c = 30;

//c = b = a;
//cout << "a = " << a << endl;
//cout << "b = " << b << endl;
//cout << "c = " << c << endl;

system("pause");

return 0;
}


  • 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include<bits/stdc++.h>

using namespace std;

class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
};

bool operator==(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}

bool operator!=(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return false;
}
else
{
return true;
}
}

string m_Name;
int m_Age;
};

void test01()
{
//int a = 0;
//int b = 0;

Person a("孙悟空", 18);
Person b("孙悟空", 18);

if (a == b)
{
cout << "a和b相等" << endl;
}
else
{
cout << "a和b不相等" << endl;
}

if (a != b)
{
cout << "a和b不相等" << endl;
}
else
{
cout << "a和b相等" << endl;
}
}


int main() {

test01();

system("pause");

return 0;
}


  • 函数调用运算符重载
  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>

using namespace std;

class MyPrint
{
public:
void operator()(string text)
{
cout << text << endl;
}

};
void test01()
{
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world");
}


class MyAdd
{
public:
int operator()(int v1, int v2)
{
return v1 + v2;
}
};

void test02()
{
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;

//匿名对象调用
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

int main() {

test01();
test02();

system("pause");

return 0;
}




6.7 继承

继承是面向对象三大特性之一

有些类与类之间存在特殊的关系,例如下图中:

1544861202252

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码



  • 继承的基本语法

例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同

接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处

继承的语法

在C++中,类继承的基本语法是在派生类的声明中使用冒号(:)跟随一个或多个基类。基类可以是publicprotectedprivate继承,这些访问修饰符决定了基类成员在派生类中的访问权限。

1
2
3
4
5
6
7
class Base {
// 基类成员
};

class Derived : public Base {
// 派生类成员
};

在这个例子中,Derived类公开地(public)继承自Base类,意味着Base类的公有成员和保护成员在Derived类中保持其原有的访问级别。

普通实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include<bits/stdc++.h>

using namespace std;

//Java页面
class Java
{
public:
void header()
{
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left()
{
cout << "Java,Python,C++...(公共分类列表)" << endl;
}
void content()
{
cout << "JAVA学科视频" << endl;
}
};
//Python页面
class Python
{
public:
void header()
{
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left()
{
cout << "Java,Python,C++...(公共分类列表)" << endl;
}
void content()
{
cout << "Python学科视频" << endl;
}
};
//C++页面
class CPP
{
public:
void header()
{
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left()
{
cout << "Java,Python,C++...(公共分类列表)" << endl;
}
void content()
{
cout << "C++学科视频" << endl;
}
};

void test01()
{
//Java页面
cout << "Java下载视频页面如下: " << endl;
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();
cout << "--------------------" << endl;

//Python页面
cout << "Python下载视频页面如下: " << endl;
Python py;
py.header();
py.footer();
py.left();
py.content();
cout << "--------------------" << endl;

//C++页面
cout << "C++下载视频页面如下: " << endl;
CPP cp;
cp.header();
cp.footer();
cp.left();
cp.content();

}

int main() {

test01();

system("pause");

return 0;
}

继承实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include<bits/stdc++.h>

using namespace std;

//公共页面
class BasePage
{
public:
void header()
{
cout << "首页、公开课、登录、注册...(公共头部)" << endl;

}

void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left()
{
cout << "Java,Python,C++...(公共分类列表)" << endl;
}

};

//Java页面
class Java : public BasePage
{
public:
void content()
{
cout << "JAVA学科视频" << endl;
}
};
//Python页面
class Python : public BasePage
{
public:
void content()
{
cout << "Python学科视频" << endl;
}
};
//C++页面
class CPP : public BasePage
{
public:
void content()
{
cout << "C++学科视频" << endl;
}
};

void test01()
{
//Java页面
cout << "Java下载视频页面如下: " << endl;
Java ja;
ja.header();
ja.footer();
ja.left();
ja.content();
cout << "--------------------" << endl;

//Python页面
cout << "Python下载视频页面如下: " << endl;
Python py;
py.header();
py.footer();
py.left();
py.content();
cout << "--------------------" << endl;

//C++页面
cout << "C++下载视频页面如下: " << endl;
CPP cp;
cp.header();
cp.footer();
cp.left();
cp.content();


}

int main() {

test01();

system("pause");

return 0;
}

总结:

继承的好处:可以减少重复的代码

class A : public B;

A 类称为 子类 或 派生类

B 类称为 父类 或 基类

派生类中的成员,包含两大部分

一类是从基类继承过来的,一类是自己增加的成员。

从基类继承过过来的表现其共性,而新增的成员体现了其个性。



  • 继承的方式

继承的语法:class 子类 : 继承方式 父类

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

image-20240331111153108

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};

// 公共继承
class Son1 :public Base1
{
public:
void func()
{
m_A; // 可访问 public权限
m_B; // 可访问 protected权限
// m_C; // 不可访问
}
};

void myClass()
{
Son1 s1;
s1.m_A; // 其他类只能访问到公共权限
}

// 保护继承
class Base2
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son2 :protected Base2
{
public:
void func()
{
m_A; // 可访问 protected权限
m_B; // 可访问 protected权限
// m_C; // 不可访问
}
};
void myClass2()
{
Son2 s;
// s.m_A; // 不可访问
}

// 私有继承
class Base3
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son3 :private Base3
{
public:
void func()
{
m_A; // 可访问 private权限
m_B; // 可访问 private权限
// m_C; // 不可访问
}
};
class GrandSon3 :public Son3
{
public:
void func()
{
// Son3是私有继承,所以继承Son3的属性在GrandSon3中都无法访问到
// m_A;
// m_B;
// m_C;
}
};


  • 继承中的对象模型

问题:从父类继承过来的成员,哪些属于子类对象中?

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C; //私有成员只是被隐藏了,但是还是会继承下去
};

//公共继承
class Son :public Base
{
public:
int m_D;
};

void test01()
{
cout << "sizeof Son = " << sizeof(Son) << endl;
}

int main() {

test01();

system("pause");

return 0;
}

利用工具查看:

image-20240331112648057

打开工具窗口后,定位到当前CPP文件的盘符

然后输入: cl /d1 reportSingleClassLayout查看的类名 所属文件名

效果如下图:

1545882158050

结论: 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到



  • 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<bits/stdc++.h>

using namespace std;

class Base
{
public:
Base()
{
cout << "Base构造函数!" << endl;
}
~Base()
{
cout << "Base析构函数!" << endl;
}
};

class Son : public Base
{
public:
Son()
{
cout << "Son构造函数!" << endl;
}
~Son()
{
cout << "Son析构函数!" << endl;
}

};


void test01()
{
// 继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
Son s;
}

int main() {

test01();

system("pause");

return 0;
}

总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反



  • 继承中 同名成员 处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<bits/stdc++.h>

using namespace std;

class Base {
public:
Base()
{
m_A = 100;
}

void func()
{
cout << "Base - func()调用" << endl;
}

void func(int a)
{
cout << "Base - func(int a)调用" << endl;
}

public:
int m_A;
};


class Son : public Base {
public:
Son()
{
m_A = 200;
}

// 当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本(重载)的同名成员函数
// 如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
void func()
{
cout << "Son - func()调用" << endl;
}
public:
int m_A;
};

void test01()
{
Son s;

cout << "Son下的m_A = " << s.m_A << endl;
cout << "Base下的m_A = " << s.Base::m_A << endl;

s.func();
s.Base::func();
s.Base::func(10);

}
int main() {

test01();

system("pause");
return EXIT_SUCCESS;
}

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数


  • 继承中 同名静态成员 处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<bits/stdc++.h>

using namespace std;

class Base {
public:
static void func()
{
cout << "Base - static void func()" << endl;
}
static void func(int a)
{
cout << "Base - static void func(int a)" << endl;
}

static int m_A;
};

int Base::m_A = 100;

class Son : public Base {
public:
static void func()
{
cout << "Son - static void func()" << endl;
}
static int m_A;
};

int Son::m_A = 200;

// 同名成员属性
void test01()
{
// 通过对象访问
cout << "通过对象访问: " << endl;
Son s;
cout << "Son 下 m_A = " << s.m_A << endl;
cout << "Base 下 m_A = " << s.Base::m_A << endl;

// 通过类名访问
cout << "通过类名访问: " << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
// 第一个::代表通过类名方式访问, 第二个::代表访问父类作用域下
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
cout << "Base 下 m_A = " << Base::m_A << endl;
}

// 同名成员函数
void test02()
{
// 通过对象访问
cout << "通过对象访问: " << endl;
Son s;
s.func();
s.Base::func();

cout << "通过类名访问: " << endl;
Son::func();
Son::Base::func();
// 出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
Son::Base::func(100);
}
int main() {

// test01();
test02();

system("pause");

return 0;
}

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)



  • 多继承语法

C++允许一个类继承多个类

语法: class 子类 :继承方式 父类1 , 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<bits/stdc++.h>

using namespace std;

class Base1 {
public:
Base1()
{
m_A = 100;
}
public:
int m_A;
};

class Base2 {
public:
Base2()
{
m_A = 200; // 开始是m_B 不会出问题,但是改为mA就会出现不明确
}
public:
int m_A;
};

// 语法:class 子类:继承方式 父类1 ,继承方式 父类2
class Son : public Base2, public Base1
{
public:
Son()
{
m_C = 300;
m_D = 400;
}
public:
int m_C;
int m_D;
};


// 多继承容易产生成员同名的情况
// 通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
}

int main() {

test01();

system("pause");

return 0;
}

总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域



  • 菱形继承

菱形继承概念

两个派生类继承同一个基类

又有某个类同时继承者两个派生类

这种继承被称为菱形继承,或者钻石继承

典型的菱形继承案例

image-20240331152052488

菱形继承问题

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<bits/stdc++.h>

using namespace std;

class Animal
{
public:
int m_Age;
};

// 继承前加virtual关键字后,变为虚继承
// 此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;

cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}


int main() {

test01();

system("pause");

return 0;
}

工具查看:cl /d1 reportSingleClassLayout查看的类名 所属文件名

没使用virtual:

使用:virtual:

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题




6.8 多态

  • 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

下面通过案例进行讲解多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include<bits/stdc++.h>

using namespace std;

class Animal
{
public:
// Speak函数就是虚函数
// 函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};

class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};

class Dog :public Animal
{
public:

void speak()
{
cout << "小狗在说话" << endl;
}

};
// 我们希望传入什么对象,那么就调用什么对象的函数
// 如果函数地址在编译阶段就能确定,那么静态联编
// 如果函数地址在运行阶段才能确定,就是动态联编

void DoSpeak(Animal & animal)
{
animal.speak();
}
//
// 多态满足条件:
// 1、有继承关系
// 2、子类重写父类中的虚函数
// 多态使用:
// 父类指针或引用指向子类对象

void test01()
{
Cat cat;
DoSpeak(cat);


Dog dog;
DoSpeak(dog);
}


int main() {

test01();

system("pause");

return 0;
}

总结:

多态满足条件

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

重写:函数返回值类型、函数名、参数列表完全一致称为重写

virtual关键字

C++中的virtual关键字是面向对象编程中的一个重要特性,它主要用于实现多态。以下是virtual关键字的详细描述:

定义和作用

  • 定义virtual是C++中用来修饰成员函数的一个关键字。当一个类中的成员函数被声明为虚函数时,就允许在派生类中重写该函数,实现多态性。
  • 作用virtual关键字使得在通过基类指针或引用调用成员函数时,能够根据对象的实际类型调用到派生类中的对应函数,而非基类中的版本。这种机制称为动态绑定后期绑定

使用场景

  • 实现多态:通过virtual关键字,可以在基类中定义虚函数,然后在派生类中重写这些虚函数,根据对象的实际类型来调用相应的函数。
  • 接口规范:在抽象基类(包含至少一个纯虚函数的类)中,使用virtual关键字定义纯虚函数,强制要求派生类必须实现这些函数。

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
virtual void display() const {
std::cout << "Display of Base" << std::endl;
}
};

class Derived : public Base {
public:
void display() const override { // C++11 引入的 override 关键字确保了重写的正确性
std::cout << "Display of Derived" << std::endl;
}
};

注意事项

  • 虚析构函数:如果一个类被用作基类,通常应该将其析构函数声明为虚析构函数,这确保了通过基类指针删除派生类对象时能够调用正确的析构函数。
  • 继承和重写:在派生类中重写虚函数时,函数的签名(包括参数类型和const修饰符)必须与基类中的虚函数完全一致,否则不会被视为重写。
  • 性能考虑:虚函数的调用比非虚函数的调用有更多的开销,因为需要通过虚函数表(vtable)来动态地解析调用。因此,当性能是关键考虑时,应谨慎使用虚函数。

通过使用virtual关键字,C++支持运行时的多态性,使得代码更加灵活和可扩展。然而,正确地使用virtual关键字也需要对C++的面向对象机制有深入的理解。

54 类和对象-多态-多态的原理剖析_哔哩哔哩_bilibili

虚函数和虚函数表

虚函数和虚函数表是C++实现多态性的核心机制。让我们深入了解这两个概念。

虚函数(Virtual Function)

虚函数是在基类中使用virtual关键字声明的成员函数,它可以在派生类中被重写(Override)。虚函数允许派生类根据对象的实际类型来调用相应的函数,实现运行时多态。这意味着,如果有一个基类指针或引用指向一个派生类对象,那么通过这个指针或引用调用虚函数时,调用的将是派生类中的版本(如果派生类中有重写的话)。

虚函数表(Virtual Table,简称vtable)

虚函数表是一个实现细节,用于支持运行时的多态性。每个使用虚函数的类都有一个虚函数表。这个表是一个编译器在编译时期生成的静态数组,用于存储指向类的虚函数的指针。每个对象都包含一个指向其类的虚函数表的指针(称为vptr),通过这个指针可以找到对应的虚函数实现。

工作原理

  1. 虚函数表的创建:编译器为每一个包含虚函数的类生成一个虚函数表。这个表包含了指向类中每个虚函数实现的指针。如果派生类重写了基类中的虚函数,则派生类的虚函数表中会存储指向这些新实现的指针。
  2. 对象的vptr:每个对象实例都会包含一个指针(vptr),指向其类的虚函数表。这个指针在对象被创建时自动设置。
  3. 虚函数的调用:当调用一个对象的虚函数时,实际上是通过对象的vptr来访问虚函数表,然后通过虚函数表找到相应函数的地址,最后调用该函数。

示例

考虑以下的类定义和函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void func() { cout << "Base::func() called" << endl; }
};

class Derived : public Base {
public:
void func() override { cout << "Derived::func() called" << endl; }
};

Base* obj = new Derived();
obj->func(); // 输出 "Derived::func() called"

在这个例子中,尽管obj的静态类型是Base*,但由于func是虚函数,并且obj实际指向Derived对象,所以调用的是Derived中的func实现。

优点与局限

  • 优点:虚函数和虚函数表提供了一种强大的机制,允许C++程序在运行时进行函数调用的决策,从而实现多态性和动态绑定。
  • 局限:虚函数的使用增加了一定的运行时开销,因为每次虚函数调用都需要通过虚函数表来解析函数地址。此外,每个对象因为包含vptr而稍微增加了内存占用。

虚函数和虚函数表是C++中实现面向对象编程核心特性之一,理解它们的工作原理对于深入学习C++非常重要。



  • 多态案例一:计算器类

案例描述:

分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include<bits/stdc++.h>

using namespace std;

// 普通实现
class Calculator {
public:
int getResult(string oper)
{
if (oper == "+") {
return m_Num1 + m_Num2;
}
else if (oper == "-") {
return m_Num1 - m_Num2;
}
else if (oper == "*") {
return m_Num1 * m_Num2;
}
// 如果要提供新的运算,需要修改源码
}
public:
int m_Num1;
int m_Num2;
};

void test01()
{
// 普通实现测试
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;

cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;

cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}



// 多态实现
// 抽象计算器类
// 多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public:

virtual int getResult()
{
return 0;
}

int m_Num1;
int m_Num2;
};

// 加法计算器
class AddCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};

// 减法计算器
class SubCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 - m_Num2;
}
};

// 乘法计算器
class MulCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};


void test02()
{
// 创建加法计算器
AbstractCalculator *abc = new AddCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; // 用完了记得销毁

// 创建减法计算器
abc = new SubCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;

// 创建乘法计算器
abc = new MulCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}

int main() {

// test01();

test02();

system("pause");

return 0;
}

总结:C++开发提倡利用多态设计程序架构,因为多态优点很多



  • 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<bits/stdc++.h>

using namespace std;

class Base
{
public:
// 纯虚函数
// 类中只要有一个纯虚函数就称为抽象类
// 抽象类无法实例化对象
// 子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};

class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};

void test01()
{
Base * base = NULL;
// base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;// 记得销毁
}

int main() {

test01();

system("pause");

return 0;
}


  • 多态案例二:制作饮品

案例描述

制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include<bits/stdc++.h>

using namespace std;

// 抽象制作饮品
class AbstractDrinking {
public:
// 烧水
virtual void Boil() = 0;
// 冲泡
virtual void Brew() = 0;
// 倒入杯中
virtual void PourInCup() = 0;
// 加入辅料
virtual void PutSomething() = 0;
// 规定流程
void MakeDrink() {
Boil();
Brew();
PourInCup();
PutSomething();
}
};

// 制作咖啡
class Coffee : public AbstractDrinking {
public:
// 烧水
virtual void Boil() {
cout << "煮农夫山泉!" << endl;
}
// 冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
// 倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
// 加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};

// 制作茶水
class Tea : public AbstractDrinking {
public:
// 烧水
virtual void Boil() {
cout << "煮自来水!" << endl;
}
// 冲泡
virtual void Brew() {
cout << "冲泡茶叶!" << endl;
}
// 倒入杯中
virtual void PourInCup() {
cout << "将茶水倒入杯中!" << endl;
}
// 加入辅料
virtual void PutSomething() {
cout << "加入枸杞!" << endl;
}
};

// 业务函数
void DoWork(AbstractDrinking* drink) {
drink->MakeDrink();
delete drink;
}

void test01() {
DoWork(new Coffee);
cout << "--------------" << endl;
DoWork(new Tea);
}


int main() {

test01();

system("pause");

return 0;
}


  • 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

1
virtual ~类名(){}

纯虚析构语法:

1
2
 virtual ~类名() = 0;
类名::~类名(){}

与其他纯虚函数不同,纯虚析构函数需要提供一个定义,原因如下:

  1. 析构过程需要调用析构函数的定义:当删除一个指向派生类对象的基类指针时,C++的析构机制会首先调用派生类的析构函数,然后沿着继承链向上调用基类的析构函数。即使是纯虚析构函数,也需要有一个定义,因为在对象销毁时,基类的析构函数最终会被调用。
  2. 防止未定义行为:如果纯虚析构函数没有定义,当它被调用时(如在派生类对象的销毁过程中),程序可能会遇到链接错误或未定义行为,因为编译器期望存在一个可调用的函数体。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<bits/stdc++.h>

using namespace std;

class Animal {
public:

Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;

// 析构函数加上virtual关键字,变成虚析构函数
// virtual ~Animal()
// {
// cout << "Animal虚析构函数调用!" << endl;
// }


virtual ~Animal() = 0;
};

Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用!" << endl;
}

// 和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。

class Cat : public Animal {
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}

public:
string *m_Name;
};

void test01()
{
Animal *animal = new Cat("Tom");
animal->Speak();

// 通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
// 怎么解决?给基类增加一个虚析构函数
// 虚析构函数就是用来解决通过父类指针释放子类对象
delete animal;
}

int main() {

test01();

system("pause");

return 0;
}

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

  3. 拥有纯虚析构函数的类也属于抽象类

纯虚构函数的缺席

通常,构造函数不可以声明为纯虚函数。构造函数的目的是初始化对象的状态,而纯虚函数的概念是要求派生类提供具体的实现。在对象构建时,基类的构造函数会在派生类构造函数之前被调用,以确保对象的基类部分被正确初始化。这意味着在构造阶段,没有逻辑上的需求或机制来支持“纯虚构造函数”,因为基类部分需要在派生类之前构建,且构造过程不能延迟到派生类提供实现。

为什么不能有虚构造函数

  • 构造阶段的类型确定性:构造函数的目的是初始化对象。在构造函数执行时,对象的类型已经被明确确定,不需要动态绑定。此外,构造函数调用遵循从基类到派生类的顺序,确保了对象的各个部分按正确的顺序被初始化。
  • vptr的初始化:在构造函数执行期间,vptr会被设置为指向当前正在构造的类的虚函数表。这意味着,在基类的构造函数中,vptr指向基类的虚函数表,在派生类的构造函数中,vptr会被更新为指向派生类的虚函数表。这个过程是自动发生的,且完全由编译器控制,以保证虚函数调用的正确性。


  • 多态案例三:电脑组装

案例描述

电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)

将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装三台不同的电脑进行工作

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#include<bits/stdc++.h>

using namespace std;

// 抽象CPU类
class CPU
{
public:
// 抽象的计算函数
virtual void calculate() = 0;
};

// 抽象显卡类
class VideoCard
{
public:
// 抽象的显示函数
virtual void display() = 0;
};

// 抽象内存条类
class Memory
{
public:
// 抽象的存储函数
virtual void storage() = 0;
};

// 电脑类
class Computer
{
public:
Computer(CPU * cpu, VideoCard * vc, Memory * mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}

// 提供工作的函数
void work()
{
// 让零件工作起来,调用接口
m_cpu->calculate();

m_vc->display();

m_mem->storage();
}

// 提供析构函数 释放3个电脑零件
~Computer()
{

// 释放CPU零件
if (m_cpu != NULL)
{
delete m_cpu;
m_cpu = NULL;
}

// 释放显卡零件
if (m_vc != NULL)
{
delete m_vc;
m_vc = NULL;
}

// 释放内存条零件
if (m_mem != NULL)
{
delete m_mem;
m_mem = NULL;
}
}

private:

CPU * m_cpu; // CPU的零件指针
VideoCard * m_vc; // 显卡零件指针
Memory * m_mem; // 内存条零件指针
};

// 具体厂商
// Intel厂商
class IntelCPU :public CPU
{
public:
virtual void calculate()
{
cout << "Intel的CPU开始计算了!" << endl;
}
};

class IntelVideoCard :public VideoCard
{
public:
virtual void display()
{
cout << "Intel的显卡开始显示了!" << endl;
}
};

class IntelMemory :public Memory
{
public:
virtual void storage()
{
cout << "Intel的内存条开始存储了!" << endl;
}
};

// Lenovo厂商
class LenovoCPU :public CPU
{
public:
virtual void calculate()
{
cout << "Lenovo的CPU开始计算了!" << endl;
}
};

class LenovoVideoCard :public VideoCard
{
public:
virtual void display()
{
cout << "Lenovo的显卡开始显示了!" << endl;
}
};

class LenovoMemory :public Memory
{
public:
virtual void storage()
{
cout << "Lenovo的内存条开始存储了!" << endl;
}
};


void test01()
{
// 第一台电脑零件
CPU * intelCpu = new IntelCPU;
VideoCard * intelCard = new IntelVideoCard;
Memory * intelMem = new IntelMemory;

cout << "第一台电脑开始工作:" << endl;
// 创建第一台电脑
Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);
computer1->work();
delete computer1;

cout << "-----------------------" << endl;
cout << "第二台电脑开始工作:" << endl;
// 第二台电脑组装
Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);;
computer2->work();
delete computer2;

cout << "-----------------------" << endl;
cout << "第三台电脑开始工作:" << endl;
// 第三台电脑组装
Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;
computer3->work();
delete computer3;

}

int main() {
test01();

return 0;
}




7. 作用域解析运算符::

作用域解析运算符(::)在C++中用于指定一个特定的作用域,这个运算符可以用于多种情况,包括访问全局变量、指定类成员、访问命名空间中的成员等。以下是作用域解析运算符的一些主要用途和示例:

  1. 访问类的静态成员

当在类的外部访问其静态成员时,使用作用域解析运算符来指定成员所属的类。

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
static int staticVar;
};

int MyClass::staticVar = 0; // 定义静态成员变量

int main() {
MyClass::staticVar = 5; // 使用作用域解析运算符访问静态成员变量
}
  1. 定义类外的成员函数

对于在类声明外定义的成员函数(包括构造函数和析构函数),使用作用域解析运算符来指定这些函数属于哪个类。

1
2
3
4
5
6
7
8
class MyClass {
public:
void myFunction();
};

void MyClass::myFunction() {
// 函数实现
}
  1. 解决命名冲突

作用域解析运算符可以用来访问被局部变量隐藏的全局变量。

1
2
3
4
5
6
int value = 5; // 全局变量

int main() {
int value = 10; // 局部变量,隐藏了同名的全局变量
std::cout << ::value; // 使用作用域解析运算符访问全局变量value
}
  1. 访问命名空间中的成员

当存在命名空间时,作用域解析运算符用于指定特定命名空间中的成员。

1
2
3
4
5
6
7
namespace MyNamespace {
int value = 5;
}

int main() {
std::cout << MyNamespace::value; // 访问命名空间中的成员
}
  1. 指定继承中的成员

在继承关系中,子类可以使用作用域解析运算符来访问被遮蔽(隐藏)的父类成员。

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
void myFunction() {}
};

class Derived : public Base {
public:
void myFunction() {
Base::myFunction(); // 调用父类中的myFunction
}
};

总结

作用域解析运算符::是C++中一个非常重要的特性,它提供了一种明确指定作用域的方式,有助于管理和访问不同作用域中的成员,解决命名冲突,以及明确地表达程序员的意图。通过恰当使用作用域解析运算符,可以增强代码的可读性和可维护性。





8. 模板

模板的概念

模板就是建立通用的模具,大大提高复用性

例如生活中的模板(PPT模版)

模板的特点:

  • 模板不可以直接使用,它只是一个框架
  • 模板的通用并不是万能的

8.1 函数模版

  • C++另一种编程思想称为 泛型编程 ,主要利用的技术就是模板
  • C++提供两种模板机制: 函数模板类模板

  • 函数模板作用

建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。

语法

1
2
template<typename T>
函数声明或定义

函数模板的声明和定义在大多数情况下需要放在一起,尤其是当模板被定义在头文件中时。这是因为函数模板的实例化发生在编译时,而不是链接时。编译器需要看到模板定义才能生成模板的实例,即具体化的函数。

解释

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<bits/stdc++.h>

using namespace std;

// 交换整型函数
void swapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

// 交换浮点型函数
void swapDouble(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}

// 利用模板提供通用的交换函数
template<typename T>
void mySwap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}

void test01()
{
int a = 10;
int b = 20;

// swapInt(a, b);

// 利用模板实现交换
// 1、自动类型推导
mySwap(a, b);

// 2、显示指定类型
mySwap<int>(a, b);

cout << "a = " << a << endl;
cout << "b = " << b << endl;

}

int main() {

test01();

system("pause");

return 0;
}

总结:

  • 函数模板利用关键字 template
  • 使用函数模板有两种方式:自动类型推导、显示指定类型
  • 模板的目的是为了提高复用性,将类型参数化


  • 函数模版注意事项
  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<bits/stdc++.h>

using namespace std;

// 利用模板提供通用的交换函数
template<class T>
void mySwap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}


// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{
int a = 10;
int b = 20;
char c = 'c';

mySwap(a, b); // 正确,可以推导出一致的T
// mySwap(a, c); // 错误,推导不出一致的T类型
}


// 2、模板必须要确定出T的数据类型,才可以使用
template<class T>
void func()
{
cout << "func 调用" << endl;
}

void test02()
{
// func(); // 错误,模板不能独立使用,必须确定出T的类型
func<int>(); // 利用显示指定类型的方式,给T一个类型,才可以使用该模板
}

int main() {

test01();
test02();

system("pause");

return 0;
}

总结:

  • 使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型


  • 函数模板案例

案例描述:

  • 利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
  • 排序规则从小到大,排序算法为选择排序
  • 分别利用char数组int数组进行测试

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<bits/stdc++.h>

using namespace std;

/* 快排 */
// --------------------------------------------------------------------------
template<typename T>
int partition(T arr[], int left, int right) { // 划分
T pivotVal = arr[left];
while (left < right) {
while (left < right && arr[right] >= pivotVal) right--;
arr[left] = arr[right];
while (left < right && arr[left] <= pivotVal) left++;
arr[right] = arr[left];
}
arr[left] = pivotVal;
return left;
}
template<typename T>
void quickSort(T arr[], int left, int right) {
if (left < right) {
int pivot = partition(arr, left, right);
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot + 1, right);
}
}
// --------------------------------------------------------------------------


template<typename T>
void printArray(T arr[], int len) {

for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
void test01()
{
//测试char数组
char charArr[] = "bdcfeagh";
int num = sizeof(charArr) / sizeof(char);
quickSort(charArr, 0, num - 1);
printArray(charArr, num);
}

void test02()
{
//测试int数组
int intArr[] = { 7, 5, 8, 1, 3, 9, 2, 4, 6 };
int num = sizeof(intArr) / sizeof(int);
quickSort(intArr, 0, num - 1);
printArray(intArr, num);
}

int main() {

test01();
test02();

system("pause");

return 0;
}

总结:模板可以提高代码复用,需要熟练掌握



  • 普通函数与函数模板区别:
  • 普通函数 调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板 调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<bits/stdc++.h>

using namespace std;

// 普通函数
int myAdd01(int a, int b)
{
return a + b;
}

// 函数模板
template<class T>
T myAdd02(T a, T b)
{
return a + b;
}

// 使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换
void test01()
{
int a = 10;
int b = 20;
char c = 'c';

cout << myAdd01(a, c) << endl; // 正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99

// myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换

myAdd02<int>(a, c); // 正确,如果用显示指定类型,可以发生隐式类型转换
}

int main() {

test01();

system("pause");

return 0;
}

总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T



  • 普通函数与函数模板的调用规则

调用规则如下:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<bits/stdc++.h>

using namespace std;

//普通函数与函数模板调用规则
void myPrint(int a, int b)
{
cout << "调用的普通函数" << endl;
}

template<typename T>
void myPrint(T a, T b)
{
cout << "调用的模板" << endl;
}

template<typename T>
void myPrint(T a, T b, T c)
{
cout << "调用重载的模板" << endl;
}

void test01()
{
//1、如果函数模板和普通函数都可以实现,优先调用普通函数
// 注意 如果告诉编译器 普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到
int a = 10;
int b = 20;
myPrint(a, b); //调用普通函数

//2、可以通过空模板参数列表来强制调用函数模板
myPrint<>(a, b); //调用函数模板

//3、函数模板也可以发生重载
int c = 30;
myPrint(a, b, c); //调用重载的函数模板

//4、 如果函数模板可以产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2); //调用函数模板
}

int main() {

test01();

system("pause");

return 0;
}

总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性


  • 函数模版声明和定义分离

同一文件下:

1
2
3
4
5
6
7
template <typename T>
void exampleFunction(T param);

template <typename T>
void exampleFunction(T param) {
// 实现细节
}


8.2 模板的局限性

局限性

  • 模板的通用性并不是万能的

例如

1
2
3
4
5
template<class T>
void f(T a, T b)
{
a = b;
}

在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了

再例如:

1
2
3
4
5
template<class T>
void f(T a, T b)
{
if (a > b) { ... }
}

在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行

因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include<bits/stdc++.h>

using namespace std;

class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
}
string m_Name;
int m_Age;
};

//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
if (a == b)
{
return true;
}
else
{
return false;
}
}


//具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
template<> bool myCompare(Person &p1, Person &p2)
{
if (p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
{
return true;
}
else
{
return false;
}
}

void test01()
{
int a = 10;
int b = 20;
//内置数据类型可以直接使用通用的函数模板
bool ret = myCompare(a, b);
if (ret)
{
cout << "a == b " << endl;
}
else
{
cout << "a != b " << endl;
}
}

void test02()
{
Person p1("Tom", 10);
Person p2("Tom", 10);
//自定义数据类型,不会调用普通的函数模板
//可以创建具体化的Person数据类型的模板,用于特殊处理这个类型
bool ret = myCompare(p1, p2);
if (ret)
{
cout << "p1 == p2 " << endl;
}
else
{
cout << "p1 != p2 " << endl;
}
}

int main() {

test01();

test02();

system("pause");

return 0;
}

总结:

  • 利用具体化的模板,可以解决自定义类型的通用化
  • 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板




8.3 类模板

  • 类模板作用

建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。

语法

1
2
template<typename T>

解释

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<bits/stdc++.h>

using namespace std;

template<class NameType, class AgeType>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};

void test01()
{
// 指定NameType 为string类型,AgeType 为 int类型
Person<string, int>P1("孙悟空", 999);
P1.showPerson();
}

int main() {

test01();

system("pause");

return 0;
}

总结:类模板和函数模板语法相似,在声明模板template后面加类,此类称为类模板



  • 类模板与函数模板区别

区别主要有两点:

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<bits/stdc++.h>

using namespace std;

//类模板
template<class NameType, class AgeType = int>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};

//1、类模板没有自动类型推导的使用方式
void test01()
{
// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导
Person <string, int>p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板
p.showPerson();
}

//2、类模板在模板参数列表中可以有默认参数
void test02()
{
Person <string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
p.showPerson();
}

int main() {

test01();

test02();

system("pause");

return 0;
}

总结:

  • 类模板使用只能用显示指定类型方式
  • 类模板中的模板参数列表可以有默认参数


  • 类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<bits/stdc++.h>

using namespace std;

class Person1
{
public:
void showPerson1()
{
cout << "Person1 show" << endl;
}
};

class Person2
{
public:
void showPerson2()
{
cout << "Person2 show" << endl;
}
};

template<class T>
class MyClass
{
public:
T obj;

//类模板中的成员函数,并不是一开始就创建的,而是在模板调用时再生成

void fun1() { obj.showPerson1(); }
void fun2() { obj.showPerson2(); }

};

void test01()
{
MyClass<Person1> m;

m.fun1();

//m.fun2();//编译会出错,说明函数调用才会去创建成员函数
}

int main() {

test01();

system("pause");

return 0;
}

总结:类模板中的成员函数并不是一开始就创建的,在调用时才去创建



  • 类模板对象做函数参数

tips:

1
typeid(T).name()

查看类型是什么

学习目标:

  • 类模板实例化出的对象,向函数传参的方式

一共有三种传入方式:

  1. 指定传入的类型 — 直接显示对象的数据类型
  2. 参数模板化 — 将对象中的参数变为模板进行传递
  3. 整个类模板化 — 将这个对象类型 模板化进行传递

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<bits/stdc++.h>

using namespace std;

//类模板
template<class NameType, class AgeType = int>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
void showPerson()
{
cout << "name: " << this->mName << " age: " << this->mAge << endl;
}
public:
NameType mName;
AgeType mAge;
};

//1、指定传入的类型 (用这种就行)
void printPerson1(Person<string, int> &p)
{
p.showPerson();
}
void test01()
{
Person <string, int >p("孙悟空", 100);
printPerson1(p);
}

//2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{
p.showPerson();
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{
Person <string, int >p("猪八戒", 90);
printPerson2(p);
}

//3、整个类模板化
template<class T>
void printPerson3(T & p)
{
cout << "T的类型为: " << typeid(T).name() << endl;
p.showPerson();

}
void test03()
{
Person <string, int >p("唐僧", 30);
printPerson3(p);
}

int main() {

test01();
test02();
test03();

system("pause");

return 0;
}

总结:

  • 通过类模板创建的对象,可以有三种方式向函数中进行传参
  • 使用比较广泛是第一种:指定传入的类型


  • 类模板与继承

当类模板碰到继承时,需要注意一下几点:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<bits/stdc++.h>

using namespace std;

template<class T>
class Base
{
T m;
};

// class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son :public Base<int> // 必须指定一个类型
{
};
void test01()
{
Son c;
}

// 类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2>
{
public:
Son2()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
};

void test02()
{
Son2<int, char> child1;
}


int main() {

test01();

test02();

system("pause");

return 0;
}

总结:如果父类是类模板,子类需要指定出父类中T的数据类型



  • 类模板成员函数类外实现

学习目标:能够掌握类模板中的成员函数类外实现

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<bits/stdc++.h>

using namespace std;

// 类模板中成员函数类外实现
template<class T1, class T2>
class Person {
public:
// 成员函数类内声明
Person(T1 name, T2 age);
void showPerson();

public:
T1 m_Name;
T2 m_Age;
};

// 构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

// 成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

void test01()
{
Person<string, int> p("Tom", 20);
p.showPerson();
}

int main() {

test01();

system("pause");

return 0;
}

总结:类模板中成员函数类外实现时,需要加上模板参数列表



  • 类模板分文件编写

学习目标:

  • 掌握类模板成员函数分文件编写产生的问题以及解决方式

问题:

  • 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决:

  • 解决方式1:直接包含.cpp源文件
  • 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp.hpp是约定的名称,并不是强制

示例

person.hpp中代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#pragma once
#include <iostream>
using namespace std;
#include <string>

template<class T1, class T2>
class Person {
public:
Person(T1 name, T2 age);
void showPerson();
public:
T1 m_Name;
T2 m_Age;
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

类模板分文件编写.cpp中代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;

//#include "person.h"
#include "person.cpp" //解决方式1,包含cpp源文件

//解决方式2,将声明和实现写到一起,文件后缀名改为.hpp
#include "person.hpp"
void test01()
{
Person<string, int> p("Tom", 10);
p.showPerson();
}

int main() {

test01();

system("pause");

return 0;
}

总结:主流的解决方式是第二种,将类模板成员函数写到一起,并将后缀名改为.hpp

.h和.hpp

.hpp.h文件在功能上是相同的,都被用来声明C++中的类、函数、模板等。它们之间的区别主要是命名约定,而非功能差异。使用哪一个主要取决于个人或项目团队的偏好。

.h 文件

  • 最初用于C语言头文件。
  • 在C++中也被广泛使用。
  • 有时用来表示可以被C和C++共同使用的代码。

.hpp 文件

  • 显式地表示这是一个C++头文件。
  • 帮助在复杂的项目中区分C头文件和C++头文件。
  • 某些项目或组织可能优先选择这种扩展名来强调代码是C++特有的,尤其是在包含模板定义时


  • 类模板与友元

学习目标:

  • 掌握类模板配合友元函数的类内和类外实现

全局函数类内实现 - 直接在类内声明友元即可

全局函数类外实现 - 需要提前让编译器知道全局函数的存在

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<bits/stdc++.h>

using namespace std;

//2、全局函数配合友元 类外实现 - 先做函数模板声明,下方在做函数模板定义,在做友元
template<class T1, class T2> class Person;

//如果声明了函数模板,可以将实现写到后面,否则需要将实现体写到类的前面让编译器提前看到
//template<class T1, class T2> void printPerson2(Person<T1, T2> & p);
template<class T1, class T2>
void printPerson2(Person<T1, T2> & p)
{
cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}

template<class T1, class T2>
class Person
{
//1、全局函数配合友元 类内实现
friend void printPerson(Person<T1, T2> & p)
{
cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}


//全局函数配合友元 类外实现
// 加空模版参数裂变,告诉编译器这是个模版声明
friend void printPerson2<>(Person<T1, T2> & p);

public:

Person(T1 name, T2 age)
{
this->m_Name = name;
this->m_Age = age;
}


private:
T1 m_Name;
T2 m_Age;

};

//1、全局函数在类内实现
void test01()
{
Person <string, int >p("Tom", 20);
printPerson(p);
}


//2、全局函数在类外实现
void test02()
{
Person <string, int >p("Jerry", 30);
printPerson2(p);
}

int main() {

//test01();

test02();

system("pause");

return 0;
}

总结:建议全局函数做类内实现,用法简单,而且编译器可以直接识别



  • 类模板案例

案例描述: 实现一个通用的数组类,要求如下:

  • 可以对内置数据类型以及自定义数据类型的数据进行存储
  • 将数组中的数据存储到堆区
  • 构造函数中可以传入数组的容量
  • 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
  • 提供尾插法和尾删法对数组中的数据进行增加和删除
  • 可以通过下标的方式访问数组中的元素
  • 可以获取数组中当前元素个数和数组的容量

文件结构:

1
2
3
├─myArray.hpp    # 类模板

└─test.cpp # 测试使用

myArray.hpp中代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#pragma once
#include <iostream>
using namespace std;

template<class T>
class MyArray
{
public:

//构造函数
MyArray(int capacity)
{
this->m_Capacity = capacity;
this->m_Size = 0;
pAddress = new T[this->m_Capacity];
}

//拷贝构造
MyArray(const MyArray & arr)
{
this->m_Capacity = arr.m_Capacity;
this->m_Size = arr.m_Size;
this->pAddress = new T[this->m_Capacity];
for (int i = 0; i < this->m_Size; i++)
{
//如果T为对象,而且还包含指针,必须需要重载 = 操作符,因为这个等号不是 构造 而是赋值,
// 普通类型可以直接= 但是指针类型需要深拷贝
this->pAddress[i] = arr.pAddress[i];
}
}

//重载= 操作符 防止浅拷贝问题
MyArray& operator=(const MyArray& myarray) {

if (this->pAddress != NULL) {
delete[] this->pAddress;
this->m_Capacity = 0;
this->m_Size = 0;
}

this->m_Capacity = myarray.m_Capacity;
this->m_Size = myarray.m_Size;
this->pAddress = new T[this->m_Capacity];
for (int i = 0; i < this->m_Size; i++) {
this->pAddress[i] = myarray[i];
}
return *this;
}

//重载[] 操作符 arr[0]
T& operator [](int index)
{
return this->pAddress[index]; //不考虑越界,用户自己去处理
}

//尾插法
void Push_back(const T & val)
{
if (this->m_Capacity == this->m_Size)
{
return;
}
this->pAddress[this->m_Size] = val;
this->m_Size++;
}

//尾删法
void Pop_back()
{
if (this->m_Size == 0)
{
return;
}
this->m_Size--;
}

//获取数组容量
int getCapacity()
{
return this->m_Capacity;
}

//获取数组大小
int getSize()
{
return this->m_Size;
}


//析构
~MyArray()
{
if (this->pAddress != NULL)
{
delete[] this->pAddress;
this->pAddress = NULL;
this->m_Capacity = 0;
this->m_Size = 0;
}
}

private:
T * pAddress; //指向一个堆空间,这个空间存储真正的数据
int m_Capacity; //容量
int m_Size; // 大小
};

测试test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "myArray.hpp"
#include <string>

void printIntArray(MyArray<int>& arr) {
for (int i = 0; i < arr.getSize(); i++) {
cout << arr[i] << " ";
}
cout << endl;
}

//测试内置数据类型
void test01()
{
MyArray<int> array1(10);
for (int i = 0; i < 10; i++)
{
array1.Push_back(i);
}
cout << "array1打印输出:" << endl;
printIntArray(array1);
cout << "array1的大小:" << array1.getSize() << endl;
cout << "array1的容量:" << array1.getCapacity() << endl;

cout << "--------------------------" << endl;

MyArray<int> array2(array1);
array2.Pop_back();
cout << "array2打印输出:" << endl;
printIntArray(array2);
cout << "array2的大小:" << array2.getSize() << endl;
cout << "array2的容量:" << array2.getCapacity() << endl;
}

//测试自定义数据类型
class Person {
public:
Person() {}
Person(string name, int age) {
this->m_Name = name;
this->m_Age = age;
}
public:
string m_Name;
int m_Age;
};

void printPersonArray(MyArray<Person>& personArr)
{
for (int i = 0; i < personArr.getSize(); i++) {
cout << "姓名:" << personArr[i].m_Name << " 年龄: " << personArr[i].m_Age << endl;
}

}

void test02()
{
//创建数组
MyArray<Person> pArray(10);
Person p1("孙悟空", 30);
Person p2("韩信", 20);
Person p3("妲己", 18);
Person p4("王昭君", 15);
Person p5("赵云", 24);

//插入数据
pArray.Push_back(p1);
pArray.Push_back(p2);
pArray.Push_back(p3);
pArray.Push_back(p4);
pArray.Push_back(p5);

printPersonArray(pArray);

cout << "pArray的大小:" << pArray.getSize() << endl;
cout << "pArray的容量:" << pArray.getCapacity() << endl;

}

int main() {

//test01();

test02();

system("pause");

return 0;
}

总结:

能够利用所学知识点实现通用的数组





9. 强制类型转换运算符

参考文章: C++四种强制类型转换介绍 - 知乎 (zhihu.com)

隐式类型转换是编译器自动隐式进行的,需要在代码中体现,而显示类型转换由程序员明确指定。

C++支持C风格的强制转换,但是C风格的强制转换可能带来一些隐患,让一些问题难以发现。

C风格的强制转换可能带来一些隐患

C风格的强制类型转换(如 (int)x(void*)ptr)在C++中仍然是支持的,主要是为了保持与C语言的兼容性。然而,C风格的转换较为粗糙,它不区分转换的类型和目的,这可能导致几种潜在问题,使得代码中的错误和安全隐患难以被发现:

  1. 类型安全问题:C风格转换不进行类型检查,允许几乎任何类型的转换,即使这些转换在逻辑上没有意义或是危险的。这种宽松的类型检查使得程序更容易出错。

  2. 隐式转换的不明确性:使用C风格转换,阅读代码的人可能难以理解转换的真正意图。比如,它可能是为了去除const属性,也可能是进行了不安全的指针转换,或者是两种类型之间的正常转换。这种不明确性使得代码难以维护和理解。

  3. 误用导致的未定义行为:由于C风格转换允许进行宽泛的转换,开发者可能不小心使用错误的转换,导致未定义行为。比如,将一个对象指针转换为一个完全不相关类型的指针,然后通过这个转换后的指针来访问数据,可能会破坏对象的内存布局。

  4. 破坏const安全:C风格转换可以轻易地移除对象的const属性,这可能导致原本不应被修改的数据被意外或非法修改,从而破坏程序的正确性。

示例

假设有以下代码:

1
2
3
4
5
6
7
8
const int ci = 10;
int* ip = (int*)&ci; // C风格转换去除了const属性
*ip = 20; // 未定义行为,因为试图修改一个const对象

class Base {};
class Derived : public Base {};
Base* b = new Base;
Derived* d = (Derived*)b; // 不安全的向下转型,如果b不是指向Derived类型的对象,行为未定义

在这些例子中,C风格的转换掩盖了潜在的问题,使它们难以通过编译器警告或错误被发现。

因此,推荐使用C++提供的四种强制类型转换运算符,因为它们能更明确地表达转换的意图,并提供了更严格的类型检查。这有助于避免上述问题,使代码更安全、更易于理解和维护。

所以C++提供了一组适用于不同场景的强制转换的函数:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast

下面对这四种转换操作的适用场景分别进行说明。





  • static_cast
1
static_cast<type>(expression)

该运算符把 expression 转换为 type 类型,主要用于基本数据类型之间的转换,如把 uint 转换为 int,把 int 转换为 double 等。

1
2
3
4
5
uint x = 1;
int y = static_cast<int>(x); // 转换正确

int x = 1;
double y = static_cast<double>(x); // 转换正确

需要注意的是:static_cast 没有运行时类型检查来保证转换的安全性,需要程序员来判断转换是否安全

1
2
3
4
5
int x = -1;
uint y = static_cast<uint>(x) // 转换错误

double x = 1.23;
int y = static_cast<int>(x) // 转换丢失精度

static_cast 还可用于类层次结构中,基类和派生类之间指针或引用的转换,但也要注意:

  • static_cast 进行上行转换是安全的,即把派生类的指针转换为基类的;
  • static_cast 进行下行转换是不安全的,即把基类的指针转换为派生类的。
1
2
3
4
5
6
7
// 上行转换,派生类→基类
Derive* d = new Derive();
Base* b = static_cast<Base*>(d);

// 下行转换,基类→派生类
Base* b = new Base();
Derive* d = static_cast<Derive*>(b);

这是因为派生类包含基类信息,所以上行转换(只能调用基类的方法和成员变量),一般是安全的;

而基类没有派生类的任何信息,而下行转换后会用到派生类的方法和成员变量,这些基类都没有,很容易“指鹿为马”,或指向不存在的空间。





  • dynamic_cast
1
dynamic_cast<type>(expression)

dynamic_cast 主要用于类层次间的上行转换或下行转换。在进行上行转换时,dynamic_caststatic_cast 的效果是一样的,但在下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全

比如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;

class Base {
public:
virtual void Say() {
cout << "I am Base." << endl;
}
};

class Derive : public Base {
public:
virtual void Say() {
cout << "I am Derive." << endl;
}
};

int main()
{
// 上行转换
Derive* d1 = new Derive();
cout << "d1: " << d1 << endl;

Base* b1 = dynamic_cast<Base*>(d1);
cout << "b1: " << b1 << endl;

// 下行转换
Base* b2 = new Base();
cout << "b2: " << b2 << endl;

Derive* d2 = dynamic_cast<Derive*>(b2);
cout << "d2: " << d2 << endl;

return 0;
}
查看输出
1
2
3
4
5
d1: 0128C6D0
b1: 0128C6D0
b2: 012924E8
d2: 00000000

在进行下行转换时,从基类 b2 到 d2 时,d2 会改为空指针(0x0),这正是 dynamic_cast 提升安全的功能。这个检查主要来自虚函数表。

在C++面向对象的思想中,虚函数是实现多态的关键机制。当一个类中有虚函数时,那么编译器就会构建出一个虚函数表来指示这些函数的地址。当用基类的指针指向派生类的对象,调用方法时就会根据虚函数表找到对应派生类的方法。

注意:B 要有虚函数,否则会编译出错static_cast则没有这个限制。

这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。





  • const_cast
1
const_cast<type>(expression)

该运算符用来修改 expression 的 constvolatile 属性。这里需要注意:expression 和 type 的类型一样的。

比如下面的代码,指针 px 由于有 const 修饰,无法直接通过其修改 x 的值,但又期望能修改 x 的值时,怎么办呢?这时就需要用到 const_cast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

int main()
{
int x = 1;
cout << "before: " << x << endl;

const int* px = &x;
// *px = 2; // 编译错误
int* py = const_cast<int*>(px);
*py = 2;

cout << "px: " << px << endl;
cout << "py: " << py << endl;
cout << "after : " << x << endl;

return 0;
}
查看输出
1
2
3
4
5
before: 1
px: 006FFE58
py: 006FFE58
after : 2

可以看出,px 和 py 指向同一个地址,但通过 py 就可以修改 x 的值了。

这是因为通过const_cast,就把 const 类型的指针 px 转换成非 const 类型的指针 py 了。





  • reinterpret_cast
1
reinterpret_cast<type>(expression)

该运算符可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

using namespace std;

int main()
{
int* p = new int(5);
uint64_t p_val = reinterpret_cast<uint64_t>(p);

cout << "p :" << p << endl;
cout << "p_val:" << hex << p_val << endl;

return 0;
}
查看输出
1
2
3
p    :00A80180
p_val:a80180

上述代码把指针 p 的地址值转换成了 uint64_t 类型的整数值。

这个转换是“最不安全”的。不推荐使用。





  • 总结

综上,在使用强制类型转换时,需要首先考虑清楚使用目的,总结如下:

  • static_cast:基本类型转换,低风险;
  • dynamic_cast:类层次间的上行转换或下行转换,低风险;
  • const_cast:去 const 属性,低风险;
  • reinterpret_cast:转换不相关的类型,高风险。