万能头 :
C++基础知识
1. 常量
在C++中,常量是使用关键字const
声明的,意味着一旦被赋值后,它的值就不能被改变。常量提供了一种保护变量不被无意或有意修改的方式,有助于提高程序的可读性和维护性。以下是一些关于C++常量的关键点:
基本常量
使用const
关键字创建常量。
必须在声明时初始化。
1 const int MAX_USERS = 100 ;
枚举常量
枚举(Enumeration)是一种用户定义的类型,它包含一组命名的整型常量。
使用enum
关键字声明。
1 enum Color { RED, GREEN, BLUE };
宏常量
使用预处理器指令#define
来定义。
宏在编译之前被处理,替换文本中所有的宏名称。
字面量常量
直接出现在代码中的值,如整数10
,浮点数3.14
,字符'A'
,字符串"Hello"
。
为什么字面量常量是常量? 字面量常量是常量,因为它们表示固定的值,不能被修改。在C++(以及其他编程语言)中,当你在代码中直接使用一个值,如数字42
、字符'A'
或字符串"Hello"
时,这些值就是字面量。字面量的特点是它们在源代码中直接出现,且它们的值在编写代码的时候就已经确定,而不是在程序运行时计算得到的。
为什么是常量?
不变性 :字面量表示的值是不变的。例如,数字5
总是5
,字符串"world"
总是"world"
。这种不变性是字面量被视为常量的直接原因。
内存效率 :在编译时,编译器知道字面量的值,并且可以进行优化。因为这些值是不变的,所以在程序的执行期间不需要为它们分配可变存储空间。
安全性 :将字面量视为常量可以防止程序意外改变它们的值,这增加了代码的安全性和预测性。
示例 :
考虑以下代码片段:
1 2 int x = 10 ; char c = 'A' ;
在这些示例中,10
和'A'
都是字面量常量。你不能去“改变”10
的值变成11
,或者改变'A'
成'B'
,它们固有地代表了它们的值。你可以改变变量x
和c
的值,但字面量10
和'A'
本身的含义是不可改变的。
结论 :
理解字面量常量作为常量的原因,帮助我们在编写代码时更清楚地理解值的不变性和表达式的含义,这对于编写可读性高、稳定和高效的代码非常重要。
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 ; constexpr int limit = 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 );
与const
的区别
const
用于定义常量,这意味着一旦赋值后,该变量的值就不能改变。但const
变量的值不一定在编译时就已知。
constexpr
表示表达式的值可以(并且必须)在编译时求值。这适用于变量、函数等。
使用场景
constexpr
在需要编译时常量表达式,例如数组大小、整数模板参数等场合特别有用。它们还可以用于性能优化,因为constexpr
允许在编译时而非运行时进行计算。
注意事项
constexpr
函数不一定总是在编译时被求值。如果你在运行时用非常量表达式作为参数调用一个constexpr
函数,该调用仍然可以工作,但计算将在运行时进行。
使用constexpr
要求编译器支持C++11或更高版本。
总之,constexpr
是C++中一个强大的特性,它提供了一种在编译时而不是运行时进行计算的方法,有助于提高程序的效率和可靠性。
常量指针与指针常量
常量指针(const
指针):指向常量的指针,不能通过该指针修改所指向的值。
指针常量:指针本身是常量,不能指向别的地址,但所指向的值可以修改。
同时使用:既不能修改指针指向,也不能通过指针修改值。
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 ; auto y = 1.5 ;
在这里,auto
使得编码更加简洁,并且能够很容易地适应类型的变化,而不需要修改每一个变量的声明。
总结 :
在C语言 中,auto
是一个几乎不被使用的关键字,因为它指明了默认的存储类别(自动存储类别),而这是局部变量的默认行为。
在C++ 中,auto
关键字被重新定义,用于启用类型推断,极大地提升了编程的便利性和代码的通用性。
这种变化体现了C++对于简化编程复杂性和提高语言灵活性的持续努力。
volatile volatile
关键字在C和C++语言中被用来告诉编译器,一个变量的值可能以编译器不预期的方式被改变。这意味着使用volatile
声明的变量不应该被编译器优化,因为它的值可能会在程序的控制流之外被修改。这个关键字主要用于两个场景:
硬件访问
当程序需要直接与硬件设备交互时,该设备的状态或数据可能会独立于程序的任何操作而改变。例如,一个硬件寄存器的值可能由外部事件(如硬件中断)而非程序本身的写操作所修改。在这种情况下,使用volatile
可以防止编译器做出假设并错误地优化掉对这些外部状态的读取或写入操作。
多线程应用
在多线程应用程序中,一个线程可能修改另一个线程可以访问的变量。如果没有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. 字面量
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 #undef 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("This is a debug message." );#undef LOG
在这个例子中,LOG
宏根据是否定义了 DEBUG
来改变行为。通过在不需要它之后使用 #undef
,可以确保它不会在后续的代码中被误用。
注意事项 :
使用 #undef
取消宏定义后,如果需要再次使用相同的宏名称,你必须重新定义它。
#undef
对后续的代码块生效,对其前面的代码没有影响。
在大型项目中,谨慎使用宏定义和 #undef
,因为它们如果被滥用,可能会使代码难以理解和维护。
尽管 #undef
可以用来限制宏的作用域,但在现代C++中,更推荐使用常量、内联函数或者模板来代替宏,以提高代码的安全性和清晰度。
#define _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS
这种用法与我们通常定义宏来创建常量或宏函数略有不同。它不是用来直接替换代码中的文本,而是作为一种特殊的标记,用于影响编译器的行为。这类宏定义主要用于配置编译环境或控制编译器的特定行为,而不是用于代码中的直接替换。
常规宏定义 :
常规的宏定义通常有以下两种形式:
定义常量 :用宏来定义一个常量值,例如:
在这里,每当预处理器遇到PI
,它都会用3.14159
来替换它。
定义宏函数 :定义一个宏函数,允许在代码中使用类似函数调用的语法来执行代码替换,例如:
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
,还包括一系列其他有用的指令,它们在编译之前处理源代码,用于条件编译、文件包含、宏定义等。以下是一些常用的预处理器指令:
#include
用于包含头文件,将文件的内容直接插入到当前位置。有两种形式:
#include <filename>
:用于标准库头文件。
#include "filename"
:用于项目内的头文件。
#define
定义宏,可以是对象(常量值)或函数(带参数的宏)。
#undef
取消已定义的宏的定义。
#if
, #elif
, #else
, #endif
用于条件编译。根据条件是否满足,决定是否编译代码段。
#ifdef
, #ifndef
检查宏是否被定义:
#ifdef
:如果宏已定义,则编译下面的代码。
#ifndef
:如果宏未定义,则编译下面的代码。
#error
当遇到此指令时,预处理器会显示一个错误消息并停止编译。
#pragma
提供了一种标准化的方法,用于向编译器传递特定的指令。编译器可能会忽略它不认识的#pragma
指令。
#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++分文件编写时还需要考虑这些特性的特定需求。下面是一些基本原则和区别:
相似之处 :
头文件(.h或.hpp) :在C和C++中,头文件用于声明函数、变量、类等。它们允许在多个源文件之间共享这些声明,避免重复代码。
源文件(.c或.cpp) :源文件包含函数、类方法的实现代码。在分文件编写中,头文件被包含在源文件中,以提供必要的声明。
预处理指令 :#include
预处理指令在C和C++中都用于包含头文件,这使得函数、类的声明在源文件中可用。
C++特有的考虑因素 :
类声明与定义 :在C++中,类通常在头文件中声明,在源文件中定义(实现)。这意味着类的成员函数在头文件中被声明,在一个或多个源文件中被定义。
模板 :C++的模板(包括类模板和函数模板)通常在头文件中完全定义。由于模板需要在编译时实例化,它们的定义(实现)通常与声明放在同一个头文件中,而不是分开到源文件中。
内联函数 :C++允许函数在头文件中被声明为inline
,这样做的目的是减少小函数的调用开销。虽然内联函数也可以在C中通过宏实现,但C++的inline
关键字提供了类型安全和更好的编程风格。
命名空间 :C++支持命名空间,这是C不具备的特性。命名空间可以用于头文件和源文件中,以避免命名冲突。
示例 :
C++头文件(myclass.hpp)
1 2 3 4 5 6 7 8 9 10 #ifndef MYCLASS_HPP #define MYCLASS_HPP class MyClass {public : void myFunction () ; };#endif
C++源文件(myclass.cpp)
1 2 3 4 5 6 #include "myclass.hpp" void MyClass::myFunction () { }
结论 :
虽然C++和C在分文件编写方面有很多相似之处,但C++的一些高级特性(如类、模板和命名空间)引入了额外的考虑因素。理解这些差异有助于有效地组织和维护C++项目的代码。
确实,你可以通过extern
关键字在C或C++中实现不同源文件间的函数或变量的共享,而不必依赖于头文件。extern
用于声明一个变量或函数是在别处定义的,这样做可以在不同的源文件之间共享同一个全局变量或函数。不过,这种方式相比于使用头文件来说,管理起来更为繁琐,特别是在大型项目中,而且容易出错。
使用extern
分文件编写的基本方法 :
假设你有两个源文件,file1.cpp
和file2.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; 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 ; }
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 ; }
指针变量指向内存中编号为0的空间
用途: 初始化指针变量
注意: 空指针指向的内存是不可以访问的
NULL 还是 nullptr? 在C++中,nullptr
和NULL
都可以用来表示空指针,但它们之间存在一些重要的区别,特别是在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 (nullptr ); }
在上面的例子中,使用nullptr
可以确保调用正确的func
函数版本,而使用NULL
可能会产生歧义。
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> using namespace std;int main () { int * p = (int *)0x1100 ; cout << *p << endl; return 0 ; }
总结 :空指针和野指针都不是我们申请的空间,因此不要访问。
常量指针与指针常量
常量指针(const
指针):指向常量的指针,不能通过该指针修改所指向的值。
指针常量:指针本身是常量,不能指向别的地址,但所指向的值可以修改。
同时使用:既不能修改指针指向,也不能通过指针修改值。
1 const int * const ptr = &var;
C++现代实用教程:智能指针_哔哩哔哩_bilibili
更多指针内容可以查看本站文章C语言笔记 | GXBLOGS :【目录】随手记>指针
C++核心知识
1. 内存
是的,C++的内存模型基本上和C语言非常相似,因为C++是以C为基础构建的,并保留了C的很多特性和概念。C++程序的内存布局通常包括以下几个区域:
栈(Stack)
用于存放函数的局部变量、函数参数以及函数的返回地址等。栈是自上而下扩展的数据结构,具有后进先出的特性。当函数调用时,新的栈帧(包含函数的局部变量和其他上下文信息)被推入栈中;当函数返回时,栈帧被弹出。
堆(Heap)
用于动态内存分配。与栈不同,堆是由程序员通过new
(在C++中)或malloc
(在C中)等操作手动分配和释放的。堆的大小不是固定的,可以动态扩展或缩小。
全局/静态存储区(Global/Static Area)
存放全局变量和静态变量。这些变量在程序的整个生命周期内都存在。在C++中,全局对象的构造和静态对象的初始化发生在main
函数执行之前,它们的析构/销毁发生在main
函数执行结束后。
常量存储区(Constants Area)
存放常量数据,如字符串字面值和其他常量表达式。这部分通常是只读的,在某些编译器实现中,它可能是.rodata
(Read-Only Data)段的一部分。
代码区(Code Area)
存放程序的二进制代码,即编译后的机器指令。这部分通常也是只读的,防止程序的指令在运行时被意外或恶意修改。
C++的一些特殊考虑 :
虽然C++和C在内存布局方面非常相似,但C++引入了对象模型和RAII(Resource Acquisition Is Initialization)等概念,使得内存管理更加复杂。例如:
对象的内存管理 :C++通过构造函数和析构函数自动管理对象的生命周期,支持对象的动态创建(在堆上)和自动销毁。
智能指针 :C++提供了智能指针如std::unique_ptr
、std::shared_ptr
等,以便更安全地管理堆上的内存。
总的来说,C++保留了C语言的内存模型,并在此基础上增加了面向对象和自动资源管理等特性,使得内存管理更为灵活和安全。
malloc和new 在C++中,malloc
和new
都可以用来在堆上分配内存,但它们之间存在一些关键的区别,包括它们分配的内存区域的处理方式。以下是malloc
和new
最主要的区别:
构造函数和析构函数
new
:分配内存的同时会调用对象的构造函数,为对象的初始化提供了机会。相应地,delete
会调用对象的析构函数,确保资源得到释放。
malloc
:仅仅分配内存,不会调用构造函数。因此,当使用malloc
分配内存给对象时,对象的成员不会被自动初始化。相应地,free
释放内存但不会调用析构函数。
类型安全
new
:返回分配对象的正确类型的指针,不需要类型转换。
malloc
:返回void*
类型的指针,需要显式转换为目标类型的指针。
错误处理
new
:在内存分配失败时,会抛出std::bad_alloc
异常(除非使用了new (nothrow)
)。
malloc
:在内存分配失败时,返回NULL
。
分配数组
使用new[]
和delete[]
可以为数组分配和释放内存,同时为数组中的每个对象调用构造函数和析构函数。
而malloc
和free
则没有这样的能力,它们仅仅分配和释放原始内存,不负责对象的构造和析构。
内存分配区域
尽管malloc
和new
在技术上都是在堆区分配内存,但实际上C++标准并没有严格规定new
和malloc
分配的内存区域必须是同一个。在大多数实现中,它们都是从同一块堆内存中分配内存的,但new
可能涉及更复杂的内存管理策略(例如,使用内存池)。因此,new
和malloc
分配的内存区域在表现上可能有细微的不同,尤其是在处理对象构造和析构时。
结论
尽管malloc
和new
都用于内存分配,但由于new
和delete
提供了类型安全、自动调用构造函数和析构函数的特性,它们是C++推荐的内存分配和释放方式。使用new
和delete
可以让代码更加安全、简洁和面向对象。
2. new
在C++中,new
关键字用于动态分配内存,并根据需要进行对象的构造。与C语言的malloc
函数相比,new
不仅分配内存,还会自动调用对象的构造函数(如果分配的是一个对象),使得内存分配和对象初始化一步到位。此外,new
还提供了类型安全,确保分配的内存类型与所需类型匹配。
基本用法 :
1 2 int * ptr = new int ; *ptr = 5 ;
在上面的例子中,new int
动态分配了足够存储一个int
类型值的内存,并返回一个指向这块内存的指针。与此同时,分配的内存被自动初始化。
带有初始化的用法 :
new
也支持在分配内存时进行初始化:
对于自定义类型:
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 ];
数组分配时不支持直接初始化每个元素,但你可以使用后面的括号来初始化POD(Plain Old Data,简单旧数据类型)类型数组的元素为零:
1 int * array = new int [10 ]();
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++中动态内存管理的基础。使用new
和delete
时,开发者需要负责匹配地使用它们,以确保每次分配的内存都被适时释放,避免内存泄漏。
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;void memoryLeak1 () { string *str = new string ("动态分配内存!" ); return ; }int memoryLeak2 () { string *str = new string ("内存泄露!" ); if (1 ) { return -1 ; } delete str; return 1 ; }int main (void ) { memoryLeak1 (); memoryLeak2 (); return 0 ; }
memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!
使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!
思考 :如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?
智能指针就是通过这个原理来解决指针自动释放的问题!
C++98 提供了 auto_ptr
模板的解决方案
C++11 增加 unique_ptr
、shared_ptr
和weak_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 ) { auto_ptr<Test> test (new Test) ; cout << "test->debug:" << test->getDebug () << endl; cout << "(*test).debug:" << (*test).getDebug () << endl; return 0 ; }
为什么智能指针可以像普通指针那样使用 ???
因为其里面重载了 *
和 ->
运算符, *
返回普通对象,而 ->
返回指针对象。
具体原因不用深究,只需知道他为什么可以这样操作就像!
函数中返回的是调用get()方法返回的值,那么这个get()
是什么呢?
智能指针的三个常用函数:
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 (_Myptr); }
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 { _Ty * _Tmp = _Myptr; _Myptr = nullptr ; return (_Tmp); }
reset()
重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
1 2 3 4 5 6 auto_ptr<Test> test (new Test) ; test.reset (); test.reset (new Test ());
reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
函数原型 :
1 2 3 4 5 6 void reset (_Ty * _Ptr = nullptr ) { if (_Ptr != _Myptr) delete _Myptr; _Myptr = _Ptr; }
尽可能不要将auto_ptr
变量定义为全局变量或指针;
1 2 auto_ptr<Test> *tp = new auto_ptr <Test>(new Test);
除非自己知道后果,不要把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;
这一操作时,你实际上是在做两件事:
转移所有权 :std::auto_ptr
有一个所谓的“独占”所有权模型,意味着一个auto_ptr
对象在任何时刻只能拥有一个对动态分配对象的所有权。当你将一个auto_ptr
赋值给另一个时(在这里是t1 = t2;
),你实际上将t2
所拥有的对象的所有权转移给了t1
。在赋值后,t1
将指向t2
之前所拥有的对象,而t2
将变为null。
可能导致悬垂指针 :由于t2
的所有权被转移了,t2
现在是空的(null)。如果你在赋值操作之后试图使用t2
,你将访问一个null指针,这可能导致未定义的行为。更糟糕的是,如果t1
之前已经拥有一个对象的所有权,那么这次赋值操作将导致t1
原来指向的对象被删除(因为auto_ptr
在转移所有权时会删除自己原来管理的对象),这就可能导致原来的资源泄露,如果还有其他指针指向这个被t1
释放的对象,它们就变成了悬垂指针。
因此,使用std::auto_ptr
时,需要非常小心地管理所有权,以避免资源泄漏和悬垂指针的问题。正是由于这种复杂且容易出错的所有权管理,std::auto_ptr
已经在C++11标准中被废弃,并被更现代且安全的智能指针如std::unique_ptr
和std::shared_ptr
所取代。这些现代智能指针提供了更清晰、更安全的所有权语义,使得资源管理更加简单和直观。
C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。
auto_ptr 被C++11抛弃的主要原因
1). 复制或者赋值都会改变资源的所有权
1 2 3 4 5 6 7 8 9 10 11 12 13 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; 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" )) ; 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
主要有三大问题:
复制和赋值会改变资源的所有权,不符合人的直觉。
在 STL 容器中使用auto_ptr
存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
不支持对象数组的操作
以上问题已经在上面体现出来了,下面将使用unique_ptr
解决这些问题。
所以,C++11用更严谨的unique_ptr
取代了auto_ptr
!
unique_ptr
和 auto_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
复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
在容器中保存指针是安全的
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;unique_ptr<string> p3 (std::move(p1)) ; p1 = std::move (p2); 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 ] = std::move (vec[1 ]); cout << "vec.at(0):" << *vec.at (0 ) << endl; cout << "vec[1]:" << *vec[1 ] << endl;
C . 支持对象数组的内存管理
1 2 unique_ptr<int []> array (new int [5 ]) ;
除了上面ABC 三项外,unique_ptr
的其余用法都与auto_ptr
用法一致。
构造
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<Test> t1;unique_ptr<Test> t2 (new Test) ; unique_ptr<int []> t3;unique_ptr<int []> t4 (new int [5 ]) ; unique_ptr<Test, DestructTest> t5;unique_ptr<Test, DestructTest> t6 (new Test) ;
删除器(Deleter) 删除器(Deleter)是与智能指针一起使用的一个对象或函数指针,用来自定义智能指针释放其所拥有资源的方式。在std::unique_ptr
的上下文中,删除器提供了一种机制,允许开发者指定当智能指针被销毁时应如何清理其所管理的资源。这是非常有用的,特别是当你需要对一些非标准资源进行管理时,例如打开的文件句柄、数据库连接、或者是需要调用特殊函数来释放的内存。
在标准用法中,std::unique_ptr
默认使用delete
或delete[]
来释放其管理的对象或数组。但是,通过提供一个自定义删除器,你可以覆盖这一默认行为,实现资源的特殊管理策略。
如何使用删除器 :
在你的示例中,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
对象。这提供了一种灵活的方式来控制资源的释放过程。
总结
自定义删除器为智能指针提供了额外的灵活性,使得资源管理更加安全和灵活。通过使用删除器,你可以确保即使在需要特殊处理的场景下,资源也能被正确管理。
赋值
1 2 3 4 unique_ptr<Test> t7 (new Test) ;unique_ptr<Test> t8 (new Test) ; t7 = std::move (t8); t7->doSomething ();
主动释放对象
1 2 3 4 unique_ptr<Test> t9 (new Test) ;
放弃对象的控制权
1 Test *t10 = t9.release ();
重置
auto_ptr
与 unique_ptr
智能指针的内存管理陷阱
1 2 3 4 5 6 7 8 9 10 auto_ptr<string> p1; string *str = new string ("智能指针的内存管理陷阱" ); p1.reset (str); { auto_ptr<string> p2; p2.reset (str); } cout << "str:" << *p1 << endl;
这是由于auto_ptr
与 unique_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; } };
引用计数的使用
调用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 )) ; 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;
→ sp1
和sp2
共同托管同一个指针,所以他们的引用计数为2;
shared_ptr< Person > sp3(sp1);
→ sp1
和sp2
和sp3
共同托管同一个指针,所以他们的引用计数为3;
构造
1). shared_ptr< T > sp1;
空的shared_ptr
,可以指向类型为T
的对象
1 2 3 shared_ptr<Person> sp1; Person *person1 = new Person (1 ); sp1.reset (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()) ;
初始化
1). 方式一:构造函数
1 2 shared_ptr<int > up1 (new int (10 )) ; shared_ptr<int > up2 (up1) ;
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 );
赋值
1 2 3 shared_ptrr<int > up1 (new int (10 )) ; shared_ptr<int > up2 (new int (11 )) ; up1 = up2;
主动释放对象
1 2 3 4 shared_ptrr<int > up1 (new int (10 )) ;
重置
首先p1
是一个指针!
p.reset() ;
将p重置为空指针,所管理对象引用计数减1
p.reset(p1);
将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d);
将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器
交换
p1
和 p2
是智能指针
1 2 std::swap (p1,p2); p1.swap (p2);
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); }int main (void ) { useTrap (); system ("pause" ); return 0 ; }
运行截图:
循环引用问题 :
在这个例子中,Boy
和Girl
类的对象互相持有对方的shared_ptr
:
当spBoy->setGirlFriend(spGirl);
被调用时,Boy
对象持有了一个指向Girl
对象的shared_ptr
,这使得Girl
对象的引用计数增加到2。
当spGirl->setBoyFriend(spBoy);
被调用时,Girl
对象持有了一个指向Boy
对象的shared_ptr
,这使得Boy
对象的引用计数增加到2。
此时,每个对象都通过shared_ptr
被另一个对象持有,形成了一个循环引用。即使在useTrap()
函数执行完毕后,spBoy
和spGirl
的生命周期结束,它们的引用计数减少了1,变成了1,但由于互相持有,它们的引用计数永远不会达到0,导致Boy
和Girl
对象都不会被析构和释放。
所以在使用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()) ; spGirl->setBoyFriend (spBoy); }
反过来也是一样的!
这是什么原因呢?
首先释放spBoy,但是因为girl对象里面的智能指针还托管着boy,boy的引用计数为2,所以释放spBoy时,引用计数减1,boy的引用计数为1;
在释放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 2 3 weak_ptr wpGirl_1; weak_ptr wpGirl_2 (spGirl) ; wpGirl_1 = spGirl;
弱指针也可以获得引用计数;
弱指针不支持 *
和 ->
对指针的访问;
在必要的使用可以转换成共享指针 lock()
;
1 2 3 4 5 shared_ptr<Girl> sp_girl; sp_girl = wpGirl_1.lock (); 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; shared_ptr<Girl> sp_girl; sp_girl = wpGirl_1.lock (); cout << sp_girl.use_count () << endl; 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; 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
:判断当前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 () { 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 2 3 4 5 6 7 int *x = new int (10 );unique_ptr< int > up1 (x) ;unique_ptr< int > up2 (x) ; 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++中非常重要的一部分,需要仔细设计以避免这类问题。
记得使用u.release()
的返回值;
在调用u.release()
时是不会释放u
所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.
禁止delete
智能指针 get
函数返回的指针;
如果我们主动释放掉 get
函数获得的指针,那么智能指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
禁止用任何类型智能指针 get
函数返回的指针去初始化另外一个智能指针!
1 2 shared_ptr< int > sp1 (new int (10 )) ;
总结:
智能指针虽然使用起来很方便,但是要注意使用智能指针的一些陷阱,否则会造成严重的内存报错或者内存泄露等问题!
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; 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) { cout << v << endl; }int main () { const int & ref = 10 ; 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
引用的方式接受临时对象,以提高效率和灵活性。
示例解释 :
这个示例试图将一个非const
引用a
绑定到字面量10
上。因为字面量10
是一个临时的右值,不能被修改,所以这是不合法的。
这里,const int &a = 10;
声明了一个const
引用a
,并将其绑定到字面量10
上。尽管10
是一个临时右值,但由于a
是const
引用,所以这是允许的。这种方式常用于函数参数传递,使得函数可以接受临时对象作为输入,同时避免了复制的开销。
结论 :
在C++中,非const
引用不能绑定到临时对象上,而const
引用可以。这反映了C++设计中的一个基本原则:通过限制对数据的修改来增加代码的安全性和灵活性。在实践中,这意味着你应该使用const
引用来传递那些不需要修改的临时对象,以提高效率和代码的可读性。
5. 函数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法: 返回值类型 函数名 (参数= 默认值){}
规则 :
如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
如果函数声明有默认值,函数实现的时候就不能有默认参数
示例:
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 ) { 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 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;void func (int &a) { cout << "func (int &a) 调用 " << endl; }void func (const int &a) { cout << "func (const int &a) 调用 " << endl; }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); func (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 ;class Circle {public : int m_r; double calculateZC () { return 2 * PI * m_r; } };int main () { Circle c1; c1.m_r = 10 ; cout << "圆的周长为: " << c1.calculateZC () << endl; system ("pause" ); return 0 ; }
C++中类的访问权限控制是面向对象编程的核心特性之一,它帮助封装和隐藏了类的内部实现,只暴露必要的接口给类的使用者。C++提供了三种访问权限修饰符:public
、protected
和private
,它们定义了类成员(包括数据成员和成员函数)的访问范围。
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 ; protectedVar = 2 ; } };int main () { MyClass obj; obj.publicVar = 1 ; }
通过正确使用访问权限修饰符,可以确保类的内部状态被适当地保护,同时向类的使用者提供一个清晰、稳定的接口。这有助于提高代码的可维护性和可扩展性。
优点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; } void setIdol (string Idol) { m_Idol = Idol; }private : string m_Name; int m_Age; string m_Idol; };int main () { Person p; p.setName ("张三" ); cout << "姓名: " << p.getName () << endl; p.setAge (50 ); cout << "年龄: " << p.getAge () << endl; p.setIdol ("马嘉祺" ); system ("pause" ); return 0 ; }
在实际的C++项目中,将类的声明和定义分别放在不同的文件中是一种常见且推荐的做法。这种做法不仅有助于代码的组织和管理,还能提高编译效率。具体来说,类的声明通常放在头文件(.h
或.hpp
文件)中,而类成员函数的定义则放在源文件(.cpp
文件)中。
类的声明 :
类的声明提供了关于类的基本信息,包括类的名称、它包含的数据成员(属性)以及成员函数(方法)的原型。类的声明基本上是类的一个接口说明,告诉使用者可以如何与类的对象进行交互,而无需了解具体的实现细节。
示例:MyClass.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #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 #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; obj.myMethod (); return 0 ; }
总结 :
将类的声明和定义分别放在头文件和源文件中是C++项目中的一种最佳实践。这种做法有助于提高代码的可读性和可维护性,同时减少不必要的编译依赖,提高编译效率。
6.2 构造函数和析构函数
对象的初始化和清理 也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数 和析构函数 解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前 系统自动调用,执行一些清理工作。
构造函数语法: 类名(){}
构造函数,没有返回值也不写void
函数名称与类名相同
构造函数可以有参数,因此可以发生重载
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
析构函数,没有返回值也不写void
函数名称与类名相同,在名称前加上符号 ~
析构函数不可以有参数,因此不可以发生重载
程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
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_ptr
、std::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;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;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; };void test01 () { Person p; }void test02 () { Person p1 (10 ) ; Person p2 = Person (10 ); Person p3 = Person (p2); Person p4 = 10 ; Person p5 = p4; }int main () { test01 (); system ("pause" ); return 0 ; } cout << "拷贝构造函数!" << endl; } ~Person () { cout << "析构函数!" << endl; }public : int age; };void test01 () { Person p; }void test02 () { Person p1 (10 ) ; Person p2 = Person (10 ); Person p3 = Person (p2); Person p4 = 10 ; Person p5 = p4; }int main () { test01 (); 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
是对另一个同类型对象的引用,该引用是常量,意味着拷贝构造函数不应修改被拷贝的对象。
拷贝构造函数的特点 :
参数 :拷贝构造函数的参数是当前类类型的一个常量引用。它不接受其他类型的参数。
作用 :用于在以下几种情况下创建一个对象的副本:
显式或隐式地将一个对象作为参数传递给函数。
从函数返回一个对象。
用一个对象初始化另一个对象。
默认行为 :如果没有为类显式定义拷贝构造函数,C++编译器会自动生成一个默认的拷贝构造函数,执行成员逐个拷贝(通常是浅拷贝)。
深拷贝 vs 浅拷贝 :如果类成员包括指向动态分配内存的指针,通常需要显式定义拷贝构造函数来实现深拷贝,以避免潜在的内存管理问题。
如果你没有为你的类显式定义一个拷贝构造函数,C++编译器会为你自动生成一个默认的拷贝构造函数。这个自动生成的拷贝构造函数执行的是浅拷贝(shallow copy),意味着它会逐个复制对象的所有成员变量到新对象中,这个复制过程是按成员的位复制(bitwise copy)进行的。
浅拷贝的含义和影响 :
对于非指针类型的成员 ,浅拷贝简单地复制其值。这对于内置数据类型(如int
、float
等)和包含这些类型的复合数据类型通常是足够的。
对于指针类型的成员 ,浅拷贝只复制指针的值,而不复制指针所指向的数据。因此,原始对象和拷贝对象的指针成员将指向同一个内存地址。这可能会导致问题,如:
当原始对象或拷贝对象被销毁时,它们的析构函数可能尝试释放同一个内存地址,造成双重释放问题(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; }
如果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; };void test01 () { Person man (100 ) ; Person newman (man) ; Person newman2 = man; }void doWork (Person p1) {}void test02 () { Person p; doWork (p); }Person doWork2 () { Person p1; cout << (int *)&p1 << endl; return p1; }void test03 () { Person p = doWork2 (); cout << (int *)&p << endl; }int main () { 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) {} 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; private : static int m_B; };int Person::m_A = 10 ;int Person::m_B = 10 ;void test01 () { 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; cout << "m_A = " << Person::m_A << 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 : static void func () { cout << "func调用" << endl; m_A = 100 ; } static int m_A; int m_B; private : static void func2 () { cout << "func2调用" << endl; } };int Person::m_A = 10 ;void test01 () { Person p1; p1.func (); Person::func (); }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 ; }
通过阅读刚才的内容,我们知道在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) { 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 (); }int main () { test01 (); system ("pause" ); return 0 ; }
常函数 :
成员函数后加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 ; } void ShowPerson () const { this ->m_B = 100 ; } void MyFunc () const { }public : int m_A; mutable int m_B; };void test01 () { const Person person; cout << person.m_A << endl; person.m_B = 100 ; person.MyFunc (); }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 { 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 { 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 () ; void visit2 () ;private : Building *building; };class 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; }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& 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; cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl; Person p4 = 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; } private : int m_A; int m_B; }; 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 ; } MyInteger operator ++(int ) { MyInteger temp = *this ; 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 (); system ("pause" ); return 0 ; }
总结: 前置递增返回引用,后置递增返回值
c++编译器至少给一个类添加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 = 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 (); 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 () { 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 继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系,例如下图中:
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码
例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同
接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处
继承的语法 在C++中,类继承的基本语法是在派生类的声明中使用冒号(:
)跟随一个或多个基类。基类可以是public
、protected
或private
继承,这些访问修饰符决定了基类成员在派生类中的访问权限。
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;class Java {public : void header () { cout << "首页、公开课、登录、注册...(公共头部)" << endl; } void footer () { cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl; } void left () { cout << "Java,Python,C++...(公共分类列表)" << endl; } void content () { cout << "JAVA学科视频" << endl; } };class Python {public : void header () { cout << "首页、公开课、登录、注册...(公共头部)" << endl; } void footer () { cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl; } void left () { cout << "Java,Python,C++...(公共分类列表)" << endl; } void content () { cout << "Python学科视频" << endl; } };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 () { cout << "Java下载视频页面如下: " << endl; Java ja; ja.header (); ja.footer (); ja.left (); ja.content (); cout << "--------------------" << endl; cout << "Python下载视频页面如下: " << endl; Python py; py.header (); py.footer (); py.left (); py.content (); cout << "--------------------" << endl; 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; } };class Java : public BasePage {public : void content () { cout << "JAVA学科视频" << endl; } };class Python : public BasePage {public : void content () { cout << "Python学科视频" << endl; } };class CPP : public BasePage {public : void content () { cout << "C++学科视频" << endl; } };void test01 () { cout << "Java下载视频页面如下: " << endl; Java ja; ja.header (); ja.footer (); ja.left (); ja.content (); cout << "--------------------" << endl; cout << "Python下载视频页面如下: " << endl; Python py; py.header (); py.footer (); py.left (); py.content (); cout << "--------------------" << endl; 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 子类 : 继承方式 父类
继承方式一共有三种:
示例:
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; m_B; } };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; m_B; } };void myClass2 () { Son2 s; }class Base3 {public : int m_A;protected : int m_B;private : int m_C; };class Son3 :private Base3 {public : void func () { m_A; m_B; } };class GrandSon3 :public Son3 {public : void func () { } };
问题 :从父类继承过来的成员,哪些属于子类对象中?
示例 :
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 ; }
利用工具查看:
打开工具窗口后,定位到当前CPP文件的盘符
然后输入: cl /d1 reportSingleClassLayout查看的类名 所属文件名
效果如下图:
结论: 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
示例 :
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 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 () { 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 ; }public : int m_A; };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 ; }
总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域
菱形继承概念 :
两个派生类继承同一个基类
又有某个类同时继承者两个派生类
这种继承被称为菱形继承,或者钻石继承
典型的菱形继承案例 :
菱形继承问题 :
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
示例 :
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; };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 : 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 (); }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 { std::cout << "Display of Derived" << std::endl; } };
注意事项
虚析构函数 :如果一个类被用作基类,通常应该将其析构函数声明为虚析构函数,这确保了通过基类指针删除派生类对象时能够调用正确的析构函数。
继承和重写 :在派生类中重写虚函数时,函数的签名(包括参数类型和const修饰符)必须与基类中的虚函数完全一致,否则不会被视为重写。
性能考虑 :虚函数的调用比非虚函数的调用有更多的开销,因为需要通过虚函数表(vtable)来动态地解析调用。因此,当性能是关键考虑时,应谨慎使用虚函数。
通过使用virtual
关键字,C++支持运行时的多态性,使得代码更加灵活和可扩展。然而,正确地使用virtual
关键字也需要对C++的面向对象机制有深入的理解。
54 类和对象-多态-多态的原理剖析_哔哩哔哩_bilibili
虚函数和虚函数表 虚函数和虚函数表是C++实现多态性的核心机制。让我们深入了解这两个概念。
虚函数(Virtual Function)
虚函数是在基类中使用virtual
关键字声明的成员函数,它可以在派生类中被重写(Override)。虚函数允许派生类根据对象的实际类型来调用相应的函数,实现运行时多态。这意味着,如果有一个基类指针或引用指向一个派生类对象,那么通过这个指针或引用调用虚函数时,调用的将是派生类中的版本(如果派生类中有重写的话)。
虚函数表(Virtual Table,简称vtable)
虚函数表是一个实现细节,用于支持运行时的多态性。每个使用虚函数的类都有一个虚函数表。这个表是一个编译器在编译时期生成的静态数组,用于存储指向类的虚函数的指针。每个对象都包含一个指向其类的虚函数表的指针(称为vptr),通过这个指针可以找到对应的虚函数实现。
工作原理
虚函数表的创建 :编译器为每一个包含虚函数的类生成一个虚函数表。这个表包含了指向类中每个虚函数实现的指针。如果派生类重写了基类中的虚函数,则派生类的虚函数表中会存储指向这些新实现的指针。
对象的vptr :每个对象实例都会包含一个指针(vptr),指向其类的虚函数表。这个指针在对象被创建时自动设置。
虚函数的调用 :当调用一个对象的虚函数时,实际上是通过对象的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 ();
在这个例子中,尽管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 () { 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 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 2 virtual ~类名() = 0 ; 类名::~类名(){}
与其他纯虚函数不同,纯虚析构函数需要提供一个定义,原因如下:
析构过程需要调用析构函数的定义 :当删除一个指向派生类对象的基类指针时,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 #include <bits/stdc++.h> using namespace std;class Animal {public : Animal () { cout << "Animal 构造函数调用!" << endl; } virtual void Speak () = 0 ; 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 ; }
总结:
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
拥有纯虚析构函数的类也属于抽象类
纯虚构函数的缺席 通常,构造函数不可以声明为纯虚函数。构造函数的目的是初始化对象的状态,而纯虚函数的概念是要求派生类提供具体的实现。在对象构建时,基类的构造函数会在派生类构造函数之前被调用,以确保对象的基类部分被正确初始化。这意味着在构造阶段,没有逻辑上的需求或机制来支持“纯虚构造函数”,因为基类部分需要在派生类之前构建,且构造过程不能延迟到派生类提供实现。
为什么不能有虚构造函数 :
构造阶段的类型确定性 :构造函数的目的是初始化对象。在构造函数执行时,对象的类型已经被明确确定,不需要动态绑定。此外,构造函数调用遵循从基类到派生类的顺序,确保了对象的各个部分按正确的顺序被初始化。
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;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 (); } ~Computer () { 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; VideoCard * m_vc; Memory * m_mem; };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; } };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 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 2 3 4 5 6 7 8 class MyClass {public : void myFunction () ; };void MyClass::myFunction () { }
解决命名冲突
作用域解析运算符可以用来访问被局部变量隐藏的全局变量。
1 2 3 4 5 6 int value = 5 ; int main () { int value = 10 ; std::cout << ::value; }
访问命名空间中的成员
当存在命名空间时,作用域解析运算符用于指定特定命名空间中的成员。
1 2 3 4 5 6 7 namespace MyNamespace { int value = 5 ; }int main () { std::cout << MyNamespace::value; }
指定继承中的成员
在继承关系中,子类可以使用作用域解析运算符来访问被遮蔽(隐藏)的父类成员。
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 (); } };
总结
作用域解析运算符::
是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 ; mySwap (a, b); 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; }void test01 () { int a = 10 ; int b = 20 ; char c = 'c' ; mySwap (a, b); }template <class T>void func () { cout << "func 调用" << endl; }void test02 () { func <int >(); }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 charArr[] = "bdcfeagh" ; int num = sizeof (charArr) / sizeof (char ); quickSort (charArr, 0 , num - 1 ); printArray (charArr, num); }void test02 () { 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; myAdd02 <int >(a, c); }int main () { test01 (); 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 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 () { int a = 10 ; int b = 20 ; myPrint (a, b); myPrint<>(a, b); int c = 30 ; myPrint (a, b, c); 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 <> 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 ) ; 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 类模板
建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型 来代表。
语法 :
解释 :
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 () { Person<string, int >P1 ("孙悟空" , 999 ); P1.showPerson (); }int main () { test01 (); system ("pause" ); return 0 ; }
总结:类模板和函数模板语法相似,在声明模板template
后面加类,此类称为类模板
区别主要有两点:
类模板没有自动类型推导的使用方式
类模板在模板参数列表中可以有默认参数
示例 :
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; };void test01 () { Person <string, int >p ("孙悟空" , 1000 ); p.showPerson (); }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 (); }int main () { test01 (); system ("pause" ); return 0 ; }
总结:类模板中的成员函数并不是一开始就创建的,在调用时才去创建
tips:
查看类型是什么
学习目标:
一共有三种传入方式:
指定传入的类型 — 直接显示对象的数据类型
参数模板化 — 将对象中的参数变为模板进行传递
整个类模板化 — 将这个对象类型 模板化进行传递
示例 :
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; };void printPerson1 (Person<string, int > &p) { p.showPerson (); }void test01 () { Person <string, int >p ("孙悟空" , 100 ); printPerson1 (p); }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); }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<int > { };void test01 () { Son c; }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.cpp" #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;template <class T1 , class T2 > class Person ;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 { 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; };void test01 () { Person <string, int >p ("Tom" , 20 ); printPerson (p); }void test02 () { Person <string, int >p ("Jerry" , 30 ); printPerson2 (p); }int main () { 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++) { 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 ; } 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 () { test02 (); system ("pause" ); return 0 ; }
总结:
能够利用所学知识点实现通用的数组
9. 强制类型转换运算符
参考文章: C++四种强制类型转换介绍 - 知乎 (zhihu.com)
隐式类型转换是编译器自动隐式进行的,需要在代码中体现,而显示类型转换由程序员明确指定。
C++支持C风格的强制转换,但是C风格的强制转换可能带来一些隐患,让一些问题难以发现。
C风格的强制转换可能带来一些隐患 C风格的强制类型转换(如 (int)x
或 (void*)ptr
)在C++中仍然是支持的,主要是为了保持与C语言的兼容性。然而,C风格的转换较为粗糙,它不区分转换的类型和目的,这可能导致几种潜在问题,使得代码中的错误和安全隐患难以被发现:
类型安全问题 :C风格转换不进行类型检查,允许几乎任何类型的转换,即使这些转换在逻辑上没有意义或是危险的。这种宽松的类型检查使得程序更容易出错。
隐式转换的不明确性 :使用C风格转换,阅读代码的人可能难以理解转换的真正意图。比如,它可能是为了去除const
属性,也可能是进行了不安全的指针转换,或者是两种类型之间的正常转换。这种不明确性使得代码难以维护和理解。
误用导致的未定义行为 :由于C风格转换允许进行宽泛的转换,开发者可能不小心使用错误的转换,导致未定义行为。比如,将一个对象指针转换为一个完全不相关类型的指针,然后通过这个转换后的指针来访问数据,可能会破坏对象的内存布局。
破坏const安全 :C风格转换可以轻易地移除对象的const
属性,这可能导致原本不应被修改的数据被意外或非法修改,从而破坏程序的正确性。
示例 :
假设有以下代码:
1 2 3 4 5 6 7 8 const int ci = 10 ;int * ip = (int *)&ci; *ip = 20 ; class Base {};class Derived : public Base {}; Base* b = new Base; Derived* d = (Derived*)b;
在这些例子中,C风格的转换掩盖了潜在的问题,使它们难以通过编译器警告或错误被发现。
因此,推荐使用C++提供的四种强制类型转换运算符,因为它们能更明确地表达转换的意图,并提供了更严格的类型检查。这有助于避免上述问题,使代码更安全、更易于理解和维护。
所以C++提供了一组适用于不同场景的强制转换的函数:
static_cast
dynamic_cast
const_cast
reinterpret_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);
这是因为派生类包含基类信息,所以上行转换(只能调用基类的方法和成员变量),一般是安全的;
而基类没有派生类的任何信息,而下行转换后会用到派生类的方法和成员变量,这些基类都没有,很容易“指鹿为马”,或指向不存在的空间。
1 dynamic_cast <type>(expression)
dynamic_cast
主要用于类层次间的上行转换或下行转换。在进行上行转换时,dynamic_cast
和 static_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
则没有这个限制。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。
1 const_cast <type>(expression)
该运算符用来修改 expression 的 const
或 volatile
属性。这里需要注意: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; 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 了。
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
:转换不相关的类型,高风险。