1. 简介
1、面向对象程序设计
面向对象的四大特性
1)封装
2)继承
3)多态
4)抽象
2、标准库
标准C++由三个部分组成
1)核心语言:提供了所有的构件块
2)C++标准库:提供了大量的函数
3)标准模版库(STL):提供了大量的方法
2. 基本语法
2.1 变量
变量的作用:变量存在的意义,方便操作内存中的数据。
(1)内存中的数据可以通过 内存地址编号 得到 (2)给内存起名称(变量)方便管理这段内存。
变量的定义、变量的声明:
变量可以多次声明,但是只能被定义一次。
可以使用extern关键字在任何地方声明一个变量。
#includeusing namespace std; // 变量声明 extern int a; extern double b; int main() { // 变量定义 int a; double b; // 实际初始化 a = 10; b = 20.0; cout << a << endl; cout << b << endl; return 0; }
2.2 常量
常量:记录程序中不可修改的数据。
定义常量的两种方式
(1)#define 常量名 常量值 // 通过宏进行定义。 (2)const 数据类型 常量名 = 常量值 // const修饰的变量
2.3 关键字
关键字(标识符)
标识符命令规则
(1)不能是关键字
(2)只能是数字、字母、下划线
(3)第一个字符必须为字母或者下划线
(4)标识符中字母区分大小写
2.4 数据类型
数据类型存在的意义:给变量分配一个合适的内存空间。
2.4.1 类型修饰符
signed
unsigned
short
long
long long
2.4.2 基本内置类型
bool 1
char 1
int 4
float 4
double 8
void 无类型
wchar_t 宽字符型
注意⚠️:使用endl,在每一行后会插入一个换行符,<<用于向屏幕传递多个值
1、 整型
2、 实型(浮点型)
默认情况下输出一个小数,会显示6位有效数字。
(1)单精度float 4。
float f1 = 3.14f; // 这里的**f代表前面的float**。默认情况下编译器把小数当成双精度,会去进行类型转换
(2)双精度double 8
3、 字符型
‘a’ 对应的ASCII码是97
‘A’ 对应的ASCII码是65
char 1
字符型变量并不是把字符本身存储到内存中存储,而是将对应的ASCII编码放入到存储单元中。
作用:用于表示单个字符
注意:
1)使用单引号括起来 2)单引号里只能有一个字符,不可以是字符串。
查看字符型变量对应的ASCII码
ch = 'a'; cout << (int)ch << endl; // 97
4、 字符串类型(使用双引号)
C语言风格的字符串:char 变量名[] = “字符串值”
C++语言风格的字符串:string 变量名 = “字符串值”。需要一个头文件 #include
5、 布尔类型bool
bool 1
6、 转义字符
\n
\t
\v
\c
\r
2.4.3 typedef 声明
为已有的类型(int/bool/char等)定义一个新的名称。
typedef type newname;
例如:typedef int feet; // feet是int类型的一个新名称
2.4.4 枚举(enumeration)类型
一个变量只有几种可能的值,则可以定义为枚举类型。(即将变量的值一一列举出来,变量的值只能在列举出的值的范围内)
例子:
enum color { red, green = 5, blue } c; c = blue;
说明:创建枚举类型color的变量c,并为c赋值为blue。
2.4.5 类型转换
将一个类型转换为另一个类型
四种类型转换:
(1)静态转换(Static Cast)
(2)动态转换(Dynamic Cast)
(3)常量转换(Constant Cast)
(4)重新解释转换(Reinterpret Cast)
确实,C++提供了四种类型转换运算符,每种都有其特定的用途。下面简要介绍这四种转换以及它们的应用场景:
1. 静态转换(Static Cast):
-
语法:static_cast
(expression) -
使用场景:它是最通用的转换形式,用于将一种类型转换为另一种类型,例如基本数据类型的转换(如 float 转 int),指向基类的指针或引用转为指向派生类的指针或引用等。
-
注意:此转换在编译时进行检查,但不执行运行时类型检查。
float f = 3.14; int i = static_cast
(f); // i will be 3 2. 动态转换(Dynamic Cast):
-
语法:dynamic_cast
(expression) -
使用场景:主要用于处理多态时的指针和引用转换。它在运行时进行检查以确保所请求的转换是安全的和有效的。
-
注意:此转换通常用于将基类指针转换为派生类指针。
class Base {}; class Derived : public Base {}; Base *b = new Derived; Derived *d = dynamic_cast
(b); // Safe conversion 3. 常量转换(Constant Cast):
-
语法:const_cast
(expression) -
使用场景:用于修改表达式的常量性,例如从const类型转换为非const类型或从volatile类型转换为非volatile类型。
-
注意:这只改变类型的const或volatile属性,并不改变实际数据。
const int ci = 10; int *nonConstIntPtr = const_cast
(&ci); 4. 重新解释转换(Reinterpret Cast):
-
语法:reinterpret_cast(expression)
-
使用场景:对任何指针或整数类型进行低级别的强制类型转换。它的用途通常是将一种类型的指针转换为另一种类型的指针。
-
注意:此转换可能是不安全的,因为它不进行任何类型检查或转换,只是简单地告诉编译器将数据重新解释为另一种类型。
int i = 10; void* ptr = reinterpret_cast
(&i); 请注意,尽管C++提供了这些强制转换运算符,但最佳实践是尽量避免使用它们,除非在某些必要的情况下。当可能的时候,总是首选C++提供的安全和自动的类型转换。
5. 数据输入/输出
cin >> a; cout >> "Hello" >> endl;
2.5 运算符
2.5.1 算数运算符
两个整数相除结果依然是整数
只有整型(int)变量可以进行取模运算
++/–区别:
++ 前置递增/后置递增
– 前置递减/后置递减
前置后置的区别:
前置,先进行变量的加1减1操作,然后进行表达式的计算。
后置,先进性表达式的计算,然后进行变量的加1减1操作。
2.5.2 关系运算符
2.5.3 逻辑运算符
&& 与
|| 或
! 非
注意⚠️:在C++中除了0都为真。
2.5.4 位运算符
位运算符作用于位,并逐位执行操作。
&、|、^(按位与运算符、按位或运算符、按位异或运算符)
~、<<、>>(取反运算符、二进制左移运算符、二进制右移运算符)
2.5.5 杂项运算符
1)sizeof:sizeof运算符 返回变量的大小
2)Condition ? X : Y 条件运算符(三目运算符)
3).(点)和->(箭头):成员运算符用于引用类、结构和共用体的成员。【访问结构的成员时使用点运算符,通过指针访问结构的成员时,使用箭头运算符】
4)Cast:强制转换运算符
5)&:指针运算符& 返回变量的地址
6)*:指针运算符* 指向一个变量
3. 程序流程结构
顺序结构
选择结构
循环结构
3.1 选择结构
1.1 三目运算符
表达式1 ? 表达式2 : 表达式3
1.2 if
1.3 switch… case…
3.2 循环结构
3.2.1 while
#include
using namespace std; // time系统时间头文件 #include int main(){ // 1、生成一个随机数 // 添加随机数种子 作用利用当前系统时间生成随机数,防止每次随机数一样的问题出现(伪随机数的出现) srand((unsigned int)time(NULL)); int num = rand() % 100 + 1; // 这样生成的是伪随机数。需要添加随机数种子,进行生成可以防止生成伪随机数。 while (1) { int val = 0; cin >> val; if (val > num) { cout << "猜测的数字过大" << endl; } else if (val < num) { cout << "猜测的数字过小" << endl; } else if (val == num) { cout << "恭喜您!猜对了" << endl; // 猜对之后退出猜测游戏。退出当前循环。 break; } } return 0; } 3.2.2 do…while
do…while与while的区别是do…while会先执行一次循环语句,再判断循环条件。
#include
using namespace std; int main(){ // 输出0~9数字 int num = 0; do { cout << num << endl; // 先执行一次循环输出0,再判断循环条件。 num ++; } while (num < 10); return 0; } 案例 水仙花数
水仙花数是三位数。每个位上的数字求三次幂之后,相加等于这个三位数。
例如:1^3 + 5^3 + 3^3 = 153
利用do…while求出所有3位数中的水仙花数。
#include
using namespace std; /* 水仙花数 水仙花数是三位数。每个位上的数字求三次幂之后,相加等于这个三位数。 例如:1^3 + 5^3 + 3^3 = 153 利用do...while求出所有3位数中的水仙花数。 */ int main(){ /* 1、打印所有三位数 2、从所有三位数中找到水仙花数 */ int num = 100; do { // 2.1 获取三位数的个位、十位、百位上的数字。 int a = 0; int b = 0; int c = 0; a = num % 10; // 获取三位数的个位上数字 b = num / 10 % 10; // 获取三位数的十位上数字。整数相除得到的也是整数。 c = num / 100; // 获取三位数的百位上数字 // 2.2 判断是否是水仙花数,是则输出这个数字。 if ((a * a * a + b * b * b + c * c * c)==num) { cout << num << endl; } num++; } while (num < 1000); return 0; } 3.2.3 for循环
案例 敲桌子
#include
using namespace std; /* 敲桌子 0~100数字。个位、十位有7,或者是7的倍数,敲桌子。 如果不是,输出这个数字。 */ int main(){ // 1、输出0~100的这些数字。 for (int i = 0; i <= 100; i++) { // 2、找出特殊数字,输出“敲桌子” if (i % 7 == 0 || i % 10 == 7 || i / 10 == 7) { cout << "敲桌子" << endl; } else { cout << i << endl; } } return 0; } 3.2.4 嵌套循环
实例 打印乘法口诀表
#include
using namespace std; int main(){ for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { int eq = i * j; cout << j << " * " << i << " = " << eq << " "; } cout << endl; } return 0; } 3.3 跳转语句(break/continue)
3.3.1 break语句
作用:跳出整个选择结构或者整个循环结构
break使用的时机
(1)、出现在switch循环中。作用是终止case并跳出switch
(2)、出现在for循环中。作用是跳出当前循环
(3)、出现在嵌套循环语句中。跳出最近的内部循环语句
案例 嵌套循环 switch语句 break
#include
using namespace std; int main(){ cout << "请选择难易程度" << endl; cout << "1、简单程度" << endl; cout << "2、中等程度" << endl; cout << "3、困难程度" << endl; int select = 0; cin >> select; switch (select) { case 1: std::cout << "简单" << std::endl; break; case 2: std::cout << "中等" << std::endl; break; case 3: std::cout << "困难" << std::endl; break; default: break; } return 0; } 案例 break的作用 ```c++ #include using namespace std; int main(){ for (int i = 1; i <= 10; i++) { for (int j = 1; j <= 10; j++) { if (j == 5) { break; } cout << " * "; } cout << endl; } return 0; } 3.3.2 continue
跳出循环中的本次循环,不再向下执行本次循环,执行下一次循环。
#include
using namespace std; int main(){ for (int i = 0; i <= 100; i++) { if (i % 2 == 0) // 对2取模为0表示这个数是偶数 { continue; // 跳出本次循环,进行下一次的循环 } cout << i << endl; } return 0; } 3.3.3 goto
跳转到另一部分代码进行执行。
4. 一维数组和指针
数组名含义:指向数据首地址的指针
4.1 指针的算数运算
& 取地址符:& 获得是一个变量的地址,返回的是指向这个变量的指针。
#include
using namespace std; int main(){ char a; double b; cout << "a的地址是" << (long long)&a << endl; // 获得指向该变量的指针(存储的是该变量的首地址) cout << "a的地址+1值是" << (long long)(&a + 1) << endl; // cout << "b的地址值是" << (long long)&b << endl; // 首地址 cout << "b的地址+1值是" << (long long)(&b + 1)<< endl; // 首地址 return 0; } /* a的地址是6128757771 a的地址+1值是6128757772 b的地址值是6128757760 b的地址+1值是6128757768 */ 4.2 数组的地址
#include
using namespace std; int main(){ int arr[5]; cout << "arr的首地址" << (long long)&arr << endl; cout << "a[0]的地址是" << (long long)&arr[0] << endl; cout << "a[1]的地址是" << (long long)&arr[1] << endl; cout << "a[2]的地址是" << (long long)&arr[2] << endl; cout << "a[3]的地址是" << (long long)&arr[3] << endl; cout << "a[4]的地址是" << (long long)&arr[4] << endl; cout << "a[0] + 1的值是" << (long long)(&arr[0] + 1) << endl; // 对指针进行算数运算 int * p = arr; cout << " " << p + 0 << endl; cout << " " << p + 1 << endl; cout << "p + 0的值" << (long long)(p + 0) << endl; cout << "p + 1的值" << (long long)(p + 1) << endl; cout << "p + 2的值" << (long long)(p + 2) << endl; return 0; } /* arr的首地址6165801972 a[0]的地址是6165801972 a[1]的地址是6165801976 a[2]的地址是6165801980 a[3]的地址是6165801984 a[4]的地址是6165801988 a[0] + 1的值是6165801976 0x16f82abf4 0x16f82abf8 p + 0的值6165801972 p + 1的值6165801976 p + 2的值6165801980 */ 注意的点:
1、 数组的特点:
1)存放具有相同数据类型的元素;
2)数组元素存放在连续的内存空间中。
2、 一维数组的三种定义方式
3、 通过下标访问数组中的数据
4、 利用循环的方式输出数组中的元素。
int arr[5] = {val1, val2, val3};
输出后的arr[3] = 0 , arr[4] = 0。
5、 一维数组名的两种用途:
1)统计数组在内存中的长度;
sizeof(arr);
2)获取数组在内存中的首地址。
cout << arr << endl;
练习1: 寻找小猪体重最大的值
#include
using namespace std; int main(){ int max = 0; int arr[5] = {300, 350, 500, 400, 250}; for (int i = 0; i < 5; i++) { if (arr[i] > max) { max = arr[i]; } } cout << "最重的小猪体重为:" << max << endl; return 0; } 练习2: 数组元素逆置(首尾元素互换)
#include
using namespace std; int main(){ int arr[] = {21, 78, 11, 54, 12}; int start = 0; int end = sizeof(arr) / sizeof(arr[0]) - 1; while (start < end) { int temp = arr[start]; arr[start] = arr[end]; arr[end] = temp; start++; end--; } for (int i = 0; i < 5; i++) { cout << arr[i] << endl; } return 0; } 4.3 数组和指针的关系
数组名相当于指向数组首元素的指针
#include
int main() { int arr[5] = {1, 2, 3, 4, 5}; // 使用数组名来访问数组元素 std::cout << "Array elements using array name:" << std::endl; for (int i = 0; i < 5; i++) { std::cout << arr[i] << " "; } std::cout << std::endl; // 使用指针来访问数组元素 int *ptr = arr; // 将数组名赋给指针 std::cout << "Array elements using a pointer:" << std::endl; for (int i = 0; i < 5; i++) { std::cout << ptr[i] << " "; } std::cout << std::endl; return 0; } /* 数组名可以视为指向数组首元素的指针。 */ 5. 二维数组
5.1 二维数组的定义/输出
#include
using namespace std; int main(){ // 二维数组的定义 // 1、第一种定义方式 int arr01[2][3] = {{1, 4, 2}, {7, 10, 3}}; // 2、第二种定义方式 int arr02[2][3]; arr02[0][0] = 1; arr02[0][1] = 4; // 3、第三种定义方式 int arr03[2][3] = {1, 4, 2, 7, 10, 3}; // 4、第四种定义方式 int arr04[][3] = {1, 4, 2, 7, 10, 3}; // 可省略行数,但是列数不能省略 // 二维数组的输出 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { cout << arr01[i][j] << " "; } } } 5.2 二维数组的数组名的作用
1)二维数组占用的内存空间
sizeof(arr)
2)二维数组的首地址
cout << arr << endl;
#include
using namespace std; int main(){ int arr[2][3] = {{1, 4, 2}, {7, 10, 3}}; // 数组名的作用 // 1. 数组占用的内存空间 cout << "二维数组占用的内存空间:" << sizeof(arr) << endl; cout << "二维数组第一行元素占用的内存空间:" << sizeof(arr[0]) << endl; cout << "二维数组第一个元素占用的内存空间:" << sizeof(arr[0][0]) << endl; cout << "二维数组的行数:" << sizeof(arr) / sizeof(arr[0]) << endl; // 2. 查看二维数组的首地址 cout << "二维数组的首地址 " << (long)arr << endl; cout << "二维数组的第一行的首地址 " << (long)arr[0] << endl; cout << "二维数组的第二行的首地址 " << (long)arr[1] << endl; cout << "二维数组的第一个元素的首地址 " << (long)&arr[0][0] << endl; // 查看一个数的首地址需要加取地址符& cout << "二维数组的第二个元素的首地址 " << (long)&arr[0][1] << endl; return 0; } 实例 成绩的统计
#include
using namespace std; #include int main(){ // 考试成绩的统计 int scores[3][3] = { {100, 100, 100}, {90, 50, 100}, {60, 70, 80} }; string names[3] = {"张三", "李四", "王五"}; for (int i = 0; i < 3; i++) { int sum = 0; for (int j = 0; j < 3; j++) { sum += scores[i][j]; } cout << names[i] << "的总分:" << sum << endl; } return 0; } 6. 函数
作用:将一段经常使用的代码进行封装,减少重复代码
6.1 函数的定义/调用
实现一个加法函数,功能:传入两个整型数据,计算数据相加的结果,并且返回。
// 定义加法函数 // num1和num2为形参 int add(int num1, int num2) // 返回值类型 函数名(参数列表){函数体 return表达式} { int sum = num1 + num2; return sum; } #include
using namespace std; int main(){ int a = 7; int b = 8; // a,b为实参 int sum = add(a, b); cout << sum << endl; return 0; } 6.2 值传递
值传递就是函数调用时实参将数值传入给形参
值传递时,如果形参发生变化,并不影响实参
#include
using namespace std; // 函数之“值传递” void swap(int num1, int num2){ cout << "交换前的值"; cout << "num1的值:" << num1 << endl; cout << "num2的值:" << num2 << endl; cout << "交换后的值"; int temp = num1; num1 = num2; num2 = temp; cout << "num1的值:" << num1 << endl; cout << "num2的值:" << num2 << endl; return; // 当函数返回值类型为void时, 可以不写,也可这样写 } int main(){ int a = 10; int b = 22; swap(a, b); cout << "值传递时,如果形参发生任何改变,并不影响实参" << endl; cout << "a的值:" << a << endl; cout << "b的值:" << b << endl; return 0; } 6.3 函数的样式
1. 无参无返
2. 无参有返
3. 有参无返
4. 有参有返
6.4 函数的声明
可以将函数往主函数后面写,提前使用函数声明告诉编译器这个函数是存在的。
#include
using namespace std; // 函数声明 // 提前告诉编译器函数的存在,可以利用函数的声明 int max(int num1, int num2); int main(){ int a = 10; int b = 20; cout << max(a, b); return 0; } int max(int num1, int num2) { return num1 > num2 ? num1 : num2; // 三目运算符 } 6.5 函数分文件编写
作用:让代码结构更加清晰
函数分文件编写步骤:
1)创建.h头文件
2)创建.cpp源文件
3)在.h头文件中写函数的声明
4)在.cpp源文件中写函数的定义
swap.h
#include
using namespace std; void swap(int a, int b); swap.cpp
#include “swap.h” void swap(int a, int b) { int tmp = a; int a = b; int b = tmp; cout << “a = ” << a << endl; cout << “b = ” << b << endl; }
func.cpp
#include “swap.h” int main() { int a = 10; int b = 12; swap(a, b); return 0; }
7. 指针
指针的作用:通过指针间接访问内存
指针存储的是内存地址
内存编号从0开始记录,一般使用十六进制数字表示
利用指针变量保存地址
7.1 指针变量的定义/使用
#include
using namespace std; int main() { // 1、指针的定义 int a = 10; // 指针定义的语法:数据类型 *指针变量名 int *p; // 让指针指向变量a的地址 p = &a; cout << "a的地址为:" << &a << endl; cout << "指针p为:" << p << endl; // 2、使用指针 // 通过解引用的方式找到指针指向的内存 // 指针前加 * 代表解引用,找到指针指向的内存中的数据 *p = 1000; cout << "a = " << a << endl; cout << "*p = " << *p << endl; return 0; } /* a的地址为:0x16b3b2ba8 指针p为:0x16b3b2ba8 a = 1000 *p = 1000 */ 7.2 指针占用的内存空间
指针也是一种数据类型,占用内存空间多大?(指针这种数据类型占用的内存大小)
在32位操作系统下占用:4个字节
在64位操作系统下占用:8个字节
#include
using namespace std; int main() { int a = 10; int *p = &a; // 查看指针数据类型占用的内存空间(注意在64/32位操作系统下,占用内存空间不同) cout << "sizeof(int *) = " << sizeof(int *) << endl; cout << "sizeof(double *) = " << sizeof(double *) << endl; return 0; } 7.3 空指针和野指针
7.3.1 空指针
空指针:指针变量指向内存编号为0的空间
用途:初始化指针变量 int *p = NULL;
注意:空指针指向的内存不能访问
// 指针变量p指向内存地址编号为0的空间
**int *p = NULL;**
// 访问空指针报错
// 内存编号0~255为系统占用内存,不允许用户访问
7.3.2 野指针/悬空指针
野指针:指针变量指向非法内存空间。不确定指针具体指向。
悬空指针:指针变量最初指向的内存已经被释放的指针。
注意:野指针/空指针都不是我们申请内存空间,因此不要访问。
7.4 const修饰指针
const修饰指针的3种情况:
1. char *const cp;
const修饰的cp在里面,所以cp指向的地址是不能改变的,但它所指向的内容是可以改变的;指针常量
2. char const *pc1;
*pc1被const所修饰,也就是指针所指向的对象,所以它指向的对象是不能改变的,但是它指向的地址是可以改变的;常量指针
3. const * const ptr
特点:指针指向不可以修改,值也不可以修改
7.5 指针和数组
作用:指针访问数组元素
注意:ptr++; // ++操作 这是因为定义的数组,申请的是连续的内存空间,当ptr++(ptr是int类型的指针变量),地址空间向后偏移是以int类型的大小为单位进行偏移的,相当于int类型数组元素的向后偏移
#include
using namespace std; int main() { int arr[10] = {11, 92, 24, 51, 78, 19, 20, 45, 61, 78}; int *ptr = arr; // 指向数组的指针(即指针指向数组首元素的的首地址) cout << arr << endl; cout << &arr[1] << endl; cout << sizeof(int *) << endl; for (int i = 0; i < 10; i++) { cout << *ptr << endl; ptr++; // 操作的是指针指向的数据 } } 7.6 指针和函数
参数传递的两种方式:
1、值传递
2、地址传递
#include
using namespace std; void swap01(int a, int b); void swap02(int *p1, int *p2); int main() { int a = 20; int b = 31; // 1、值传递 swap01(a, b); cout << "a = " << a << endl; cout << "b = " << b << endl; // 2、地址传递 // 如果是地址传递,可以修饰实参 swap02(&a, &b); cout << "a = " << a << endl; cout << "b = " << b << endl; } void swap01(int a, int b) { int temp = a; a = b; b = temp; cout << "swap01 a = " << a << endl; cout << "swap01 b = " << b << endl; } void swap02(int *p1, int *p2) { int temp = *p1; *p1 = *p2; *p2 = temp; cout << "swap02 *p1 = " << *p1 << endl; cout << "swap02 *p2 = " << *p2 << endl; } 7.7 指针、数组、函数
数组名可以表示数组的首地址,即int *arr = arr
实例
实例描述:封装一个函数,利用冒泡排序,实现对整型数组的升序排序
#include
using namespace std; void bubblesort(int *arr, int len); void printArray(int *arr, int len); int main() { int arr[5] = {1, 10, 42, 3, 0}; int len = sizeof(arr) / sizeof(arr[0]); bubblesort(arr, len); // 数组名可以表示数组的首地址,即int *arr = arr; printArray(arr, len); } void bubblesort(int *arr, int len) { for (int i = 0; i <= len - 1; i++) { for (int j = 0; j < len - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } void printArray(int *arr, int len) { for (int i = 0; i < 5; i++) { cout << arr[i] << endl; } } 7.8 多级指针
int a = 10; // 定义一个整型变量 a int *p = &a; // 定义一个指针 p,它指向 a 的地址 int **q = &p; // 定义一个二级指针 q,它指向 p 的地址 cout << a << endl; // 输出 a 的值,为 10 cout << *p << endl; // 输出 p 所指向的变量的值,为 10 cout << **q << endl; // 输出 q 所指向的指针所指向的变量的值,为 10
7.9 智能指针
自动垃圾回收(自动管理生存期,防止内存泄漏)
智能指针的分类和用法:
在C++中,智能指针是一种对象,它表现得像指针,但有自动内存管理的功能。当智能指针被销毁或超出作用范围时,它指向的内存会被自动释放。下面是C++中最常见的三种智能指针及其用法示例:
1. std::unique_ptr
std::unique_ptr 是一种独占式智能指针,它指向的对象在任何时刻都是唯一的,没有其他的智能指针可以指向相同的对象。当 unique_ptr 超出作用范围时,它会自动销毁所指向的对象。
示例:
#include
#include class MyClass { public: void classMethod() { std::cout << "MyClass method called!" << std::endl; } }; int main() { std::unique_ptr uniquePtr = std::make_unique (); uniquePtr->classMethod(); // 调用方法 return 0; } // uniquePtr 超出作用范围,其指向的内存被自动释放 2. std::shared_ptr
std::shared_ptr 是一种共享式智能指针,多个 shared_ptr 可以指向同一个对象,该对象和其相关资源会在最后一个指向它的 shared_ptr 被销毁时释放。
示例:
#include
#include class MyClass { public: ~MyClass() { std::cout << "MyClass destroyed!" << std::endl; } }; int main() { std::shared_ptr sharedPtr1 = std::make_shared (); { std::shared_ptr sharedPtr2 = sharedPtr1; // 此时,sharedPtr1 和 sharedPtr2 都指向同一个对象 } // sharedPtr2 超出作用范围,但由于 sharedPtr1 仍然存在,所以对象不会被销毁 return 0; } // sharedPtr1 超出作用范围,它指向的对象现在被销毁 3. std::weak_ptr
std::weak_ptr 是一种非拥有性智能指针,它指向一个由 std::shared_ptr 管理的对象,但不增加该对象的引用计数。weak_ptr 主要用来解决 shared_ptr 之间的循环引用问题。
示例:
#include
#include class MyClass { public: ~MyClass() { std::cout << "MyClass destroyed!" << std::endl; } }; int main() { std::weak_ptr weakPtr; { std::shared_ptr sharedPtr = std::make_shared (); weakPtr = sharedPtr; // 在这里,由于 weakPtr 不增加引用计数,所以即使 weakPtr 存在, // 当 sharedPtr 超出作用范围时,它指向的对象也会被销毁。 } // sharedPtr 超出作用范围 // 在这里,由于对象已被销毁,因此 weakPtr 现在为空。 if (weakPtr.expired()) { std::cout << "Object pointed to by weakPtr has been destroyed." << std::endl; } return 0; } 可以看到不同类型的智能指针是如何在不同情况下管理内存的。使用智能指针可以自动管理内存,确保再合适的时间释放对象占用的内存,减少内存泄漏(已分配内存,未进行释放,且程序已经失去对这部分内存的控制或引用,无法使用或释放它) 的风险。
4. auto,C++11已经弃用
7.9.1 智能指针的底层实现
智能指针在 C++ 中是一个类,其目的是模仿原生指针的行为,同时提供自动化的内存管理。智能指针的底层实现依赖于 C++ 的对象和内存管理特性,包括构造函数、析构函数、重载的操作符等等。这里我们以两种最常见的智能指针为例,详细说明其底层是如何实现的:
1. std::unique_ptr:
std::unique_ptr 实现了独占所有权的概念,意味着同一时间只有一个 unique_ptr 可以指向某个内存区域。当这个 unique_ptr 被销毁时,它所指向的内存也会被释放。
核心实现机制:
- 构造函数:用于绑定到原始指针,获取对应内存的所有权。
- 移动构造函数和移动赋值操作符:unique_ptr 不能被复制,但可以移动,意味着它们的所有权可以转移。
- 析构函数:当 unique_ptr 的生命周期结束时,析构函数会自动被调用,释放其指向的内存。
- 重载的箭头和解引用操作符:使 unique_ptr 在使用时更接近原始指针。
2. std::shared_ptr:
std::shared_ptr 实现了共享所有权的概念,允许多个 shared_ptr 指向同一个内存区域。shared_ptr 使用引用计数来跟踪指向某块内存的智能指针数量。只有当最后一个 shared_ptr 不再指向该内存时,该内存才会被释放。
核心实现机制:
- 构造函数:类似于 unique_ptr,用于绑定到原始指针。
- 拷贝(复制)构造函数和赋值操作符:允许指针所有权的共享。每次复制时,引用计数会增加。
- 析构函数:减少引用计数,并在引用计数变为零时释放内存。
- 重载的箭头和解引用操作符:使其行为类似原始指针。
除此之外,shared_ptr 通常会使用一个控制块来存储引用计数和其他管理信息。这个控制块是在堆上分配的额外内存,用来跟踪有多少个 shared_ptr 和 weak_ptr(另一种相关的智能指针,不增加引用计数但可以观察对象)指向同一个资源。
这两种智能指针都是通过 RAII (Resource Acquisition Is Initialization) 模式来管理资源,即在构造时获取资源,在析构时释放资源。通过这种方式,智能指针能有效预防内存泄露,确保在出现异常时资源能被正确释放。
7.9.2 智能指针特性的例子
1、unique_ptr
#include
class MyClass { int data; public: MyClass(int d) : data(d) {} }; int main() { std::unique_ptr ptr1 = std::make_unique (10); // 创建一个指向MyClass的unique_ptr // std::unique_ptr ptr2 = ptr1; // 编译错误,因为不能复制unique_ptr std::unique_ptr ptr3 = std::move(ptr1); // 现在ptr3拥有对象,ptr1不再指向对象 // 当ptr3离开作用域时,对象会被自动删除 return 0; } 2、shared_ptr
#include
int main() { std::shared_ptr ptr1 = std::make_shared (20); // 引用计数为1 std::shared_ptr ptr2 = ptr1; // 引用计数增加到2 ptr2.reset(); // 引用计数减少到1 // 当ptr1离开作用域时,引用计数变为0,对象被删除 return 0; } 3、weak_ptr
#include
int main() { std::shared_ptr shared = std::make_shared (30); std::weak_ptr weak = shared; std::shared_ptr shared2 = weak.lock(); // 从weak_ptr创建shared_ptr shared.reset(); // 释放对象的所有权 if(weak.expired()) { // 检查对象是否还活着 std::cout << "Object has been deleted!" << std::endl; } return 0; } 8. 引用
8.1 指针和引用的不同
引用 r,是 year 的一个别名,在内存中 r 和 year 占有同一个存储单元,指针不同,指针有自己的内存空间。
1)不存在空引用。引用必须连接到一块合法的内存;存在空指针。
int x = 42; int &ref = x; // 合法的引用,连接到 x // int &ref2; // 错误!引用必须在声明时初始化
2)一旦引用被初始化为一个对象,就不能被指向到另一个对象了。指针可以在任何时候指向到另一个对象;
int a = 10, b = 20; int &ref = a; // 引用 ref 连接到 a // ref = b; // 错误!不能改变引用 ref 连接的对象 int *ptr = &a; // 指针 ptr 存储 a 的地址 ptr = &b; // 合法,指针 ptr 指向了 b
3)引用必须在声明时被初始化。指针可以在任何时间被初始化。
int x = 42; int &ref = x; // 创建引用并初始化 // int &ref; // 错误!引用必须在创建时初始化 int *ptr; // 创建指针 ptr = &x; // 指针在稍后初始化为 x 的地址
4)引用本身没有地址,只是对象的一个别名。
指针是附属在内存位置上的标签,引用是原变量的别名。可以通过原始变量名称或引用访问变量的内容
- 通过原始变量名访问变量名称:int i = 7;
- 通过引用:
int& r = i; // 将i声明为引用变量
double& s = d; // 初始化为d的double类型的引用s。
8.2 实例
#include
using namespace std; int main() { int i; int j; int &r = i; int &s = d; i = 7; cout << "i = " << i << endl; cout << "r = " << r << endl; j = 119; cout << "j = " << j << endl; cout << "s = " << s << endl; return 0; } 8.3 引用使用
作用:给变量起别名
数据类型 &别名 = 原名;
无论操作原名还是别名都是操作的同一块内存空间。
#include
using namespace std; int main() { int i; int j; int &r = i; int &s = j; // 无论对原名还是引用名操作,都是操作的同一块内存 i = 7; cout << "i = " << i << endl; cout << "r = " << r << endl; j = 119; cout << "j = " << j << endl; cout << "s = " << s << endl; return 0; } /* i = 7 r = 7 j = 119 s = 119 */ 8.4 引用注意事项
1)引用必须初始化
int a = 10; int &b = a;
2)引用初始化后不能再改变
int a = 10; int b = 12; int &c = a; int &c = b; // 这是错误的,引用初始化后不能再进行改变
8.5 引用作函数参数
作用:函数传参时,利用引用让形参修饰实参。
三种参数传递方式:
-
数值传递
不能修饰实参
-
地址传递
可以修饰实参
(1)传入函数的指针是原指针的一个拷贝(此时存在两个指针,两个指针指向一个内存空间,同时指向原对象);
(2)在函数中不改变拷贝指针的指向时,修改拷贝指针指向的值,相当于修改原指针指向的值;
(3)在函数中改变拷贝指针的指向时,只是改变了拷贝指针的指向,不改变原指针的指向,所以不改变原指针指向的对象。
-
引用传递
可以修饰实参
需要初始化才可以使用,初始化后不能更改
(1)引用作为函数参数传递时,实质上传递的是实参本身
#include
using namespace std; // 1、数值传递 void swap01(int a, int b) { int temp = a; a = b; b = temp; } // 2、地址传递 void swap02(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } // 3、引用传递 void swap03(int &a, int &b) { int temp = a; a = b; b = temp; } int main() { int a = 10; int b = 20; /* // 数值传递 swap01(a, b); cout << a << endl; cout << b << endl; 10 20 */ /* // 地址传递 swap02(&a, &b); cout << a << endl; cout << b << endl; 20 10 */ /* // 引用传递 swap03(a, b); cout << a << endl; cout << b << endl; 20 10 */ } 8.6 引用做函数的返回值
注意:
- 不要返回局部变量(因为局部变量存储在栈区,函数运行结束后编译器释放掉这部分内存)的引用。可以通过给局部变量加static的方式,变成全局变量(全局变量存放在全局区,全局区是在程序运行结束后释放),就可以返回了。
- 函数调用可以作为左值。
#include
using namespace std; // 不要返回局部变量的引用 int &func() { int a = 10; return a; } int &func02() { // 通过加static使其存在于全局区,函数执行结束后不会被释放掉这块内存 static int a = 10; return a; } int main() { // int &ref = func(); // cout << ref << endl; int &ref = func02(); cout << ref << endl; // 10 // 作为左值 func02() = 1000; // a = 1000 cout << ref << endl; // 1000 return 0; } 8.7 引用的本质
引用本质上是 指针常量(指针常量的特点:指向不可以改,值可以改)
8.8 常量引用
作用:常量引用主要用于修饰形参,防止误操作。
引用必须引到一块合法的内存空间
const int &ref = 10; // 编译器将代码修改为 int temp = 10; const int &ref = temp;
int &ref = 10; // 这样写是错的
9. 结构体
用户自定义的数据类型,允许用户存储不同的数据类型
#include
using namespace std; #include // 结构体的定义 struct Student { string name; int age; int score; } stu3; int main() { // 通过结构体创建结构体变量的三种方式 // 1、struct 结构体名 变量名; // 2、struct 结构体名 变量名 = {成员1值,成员2值}; // 3、定义结构体时,顺便创建变量 struct Student stu1; // struct 关键字可以省略 stu1.name = "张三"; stu1.age = 12; stu1.score = 100; struct Student stu2 = {"李四", 42, 98}; stu3.name = "王五"; stu3.age = 8; stu3.score = 99; return 0; } 9.1 结构体数组
将自定义的结构体放入到数组中方便维护
#include
using namespace std; #include // 结构体 struct student { string name; int age; int score; }; int main() { // 2、创建结构体数组 struct student arr[3] = { {"张三", 12, 99}, {"王五", 54, 100}, {"李四", 66, 79}}; // 3、给结构体数组中的元素赋值 arr[2].name = "赵六"; arr[2].age = 60; arr[2].score = 59; // 4、遍历结构体数组 for (int i = 0; i < 3; i++) { cout << "name:" << arr[i].name << " age:" << arr[i].age << " score:" << arr[i].score << endl; } return 0; } 9.2 结构体指针
通过“->”访问结构体变量中的成员属性
#include
using namespace std; // 通过指针访问结构体变量的成员属性值 // 定义结构体 struct student { string name; int age; int score; }; int main() { // 创建结构体变量并赋值 struct student stu = {"张三", 12, 88}; // 通过指针访问结构体变量的成员属性值 struct student *p = &stu; cout << "name: " << p->name << " age:" << p->age << " score:" << p->score << endl; return 0; } 9.3 结构体嵌套结构体
结构体中的成员是另一个结构体
#include
using namespace std; #include struct student { string name; int age; int score; }; struct teacher { int id; string name; int age; struct student stu; }; int main() { struct teacher t1; t1.id = 11; t1.name = "王老师"; t1.age = 32; t1.stu.name = "李学生"; t1.stu.age = 22; t1.stu.score = 88; cout << "老师id:" << t1.id << " 老师姓名:" << t1.name << " 老师年龄:" << t1.age << " 学生姓名:" << t1.stu.name << " 学生年龄:" << t1.stu.age << " 学生分数:" << t1.stu.score << endl; return 0; } 9.4 结构体做函数参数
9.4.1 结构体作为 值传递
9.4.2 结构体作为 地址传递
#include
using namespace std; #include void printArray01(struct student stu); void printArray02(struct student *p); // 通过指针访问结构体变量的成员属性值 // 定义结构体 struct student { string name; int age; int score; }; int main() { // 创建结构体变量并赋值 struct student stu = {"张三", 12, 88}; // 1、值传递 printArray01(stu); // 2、地址传递 printArray02(&stu); return 0; } void printArray01(struct student stu) { cout << "name:" << stu.name << " age:" << stu.age << " score:" << stu.score << endl; return; } void printArray02(struct student *p) { cout << "name: " << p->name << " age:" << p->age << " score:" << p->score << endl; return; } 9.5 结构体中const使用场景
作用:使用const防止误操作
#include
using namespace std; #include void printArray(struct student *p); struct student { string name; int age; int score; }; int main() { struct student stu; stu.name = "张三"; stu.age = 28; stu.score = 100; printArray(&stu); return 0; } void printArray(const struct student *p) // 参数是地址传递(提高代码性能,值传递是对值的复制,如果是指针的话,只需要占用4/8个字节),如果修改就会修改原始数据,加const防止误修改** { cout << "name:" << p->name << " age:" << p->age << " score:" << p->score << endl; return; } 案例1
#include
using namespace std; #include #include void allocateSpace(struct Teacher t[], int len); void printInfo(struct Teacher t[], int len); struct Student { string sName; int score; }; struct Teacher { string tName; struct Student sArray[5]; }; int main() { // 随机数种子,使生成随机数的时候,真正随机起来 srand((unsigned int)time(NULL)); struct Teacher t[3]; int len = sizeof(t) / sizeof(t[0]); allocateSpace(t, len); printInfo(t, len); } void allocateSpace(struct Teacher t[], int len) { string nameSeed = "ABCDE"; for (int i = 0; i < len; i++) { t[i].tName = "Teacher_"; t[i].tName += nameSeed[i]; for (int n = 0; n < 5; n++) { t[i].sArray[n].sName = "Student_"; t[i].sArray[n].sName += nameSeed[n]; t[i].sArray[n].score = rand() % 61 + 40; // 生成0~60之间的随机数,分数在40~100之间 } } } void printInfo(struct Teacher t[], int len) { for (int i = 0; i < len; i++) { cout << t[i].tName << endl; for (int j = 0; j < 5; j++) { cout << "sName:" << t[i].sArray[j].sName << " score:" << t[i].sArray[j].score << endl; } } } 案例2
创建结构体数组,并按照age进行冒泡升序排序,打印结果
#include
using namespace std; #include void printInfo(struct hero hArray[], int len); void sortArray(struct hero hArray[], int len); struct hero { string name; int age; string gender; }; int main() { struct hero hArray[5] = { {"刘备", 23, "男"}, {"关羽", 22, "男"}, {"张飞", 20, "男"}, {"赵云", 21, "男"}, {"貂蝉", 19, "女"}}; int len = sizeof(hArray) / sizeof(hArray[0]); printInfo(hArray, len); sortArray(hArray, len); printInfo(hArray, len); } void printInfo(struct hero hArray[], int len) { for (int i = 0; i < len; i++) { cout << "name:" << hArray[i].name << " age:" << hArray[i].age << " gender:" << hArray[i].gender << endl; } } void sortArray(struct hero hArray[], int len) { for (int i = 0; i < len - 1; i++) { for (int j = 0; j < len - i - 1; j++) { if (hArray[j].age > hArray[j + 1].age) { // 注意这一块,可以直接写成交换数组元素位置的方式 struct hero temp = hArray[j]; hArray[j] = hArray[j + 1]; hArray[j + 1] = temp; } } } } C++核心编程
1. 内存分区模型
1.1 程序运行前、运行后
C++程序执行时,将内存划分为4个区域
1、代码区
1)存放CPU执行的指令;
2)共享:需要频繁执行的程序,只需要在内存中保留一份即可;
3)只读:防止程序意外修改指令。
2、全局区:存放全局变量、静态变量以及常量;
全局区还包含常量区,字符串常量和其他常量(const修饰的一些变量)也存放在此处
该区域的数据在程序结束后由操作系统释放。
程序运行前存在的区域:代码区、全局区
程序运行后的区域:栈区、堆区
3、栈区:编译器自动分配释放,存放局部变量、函数的参数值等。
1)函数运行结束后,编译器将局部变量自动释放
2)不要返回局部变量(栈区的数据)的地址,因为函数运行结束后,变量内存(栈区的数据)被释放,再使用指针进行操作就是非法操作了。
#include
using namespace std; // 栈区存放的数据 以及栈区需要注意的点 int *fun(int b) { int a = 10; // 局部变量 存放在栈区,栈区的数据在**函数执行结束之后**由编译器自动释放 return &a; // 返回局部变量的地址 } int main() { int b = 65; int *p = fun(b); cout << *p << endl; // 第一次可以打印出 10 是因为编译器做了保留 cout << *p << endl; // 第二次打印的时候,乱码 编译器是放掉了这块内存 return 0; } 4、堆区:程序员分配释放,程序员不释放,程序结束时,由操作系统回收。
在C++中主要利用new在堆区中开辟内存。对象销毁前,使用delete释放
#include
using namespace std; int *fun() { // 使用new关键字 将数据开辟到堆区 // 指针 本质上也是局部变量,放在栈上,指针保存的数据是放在堆区 int *p = new int(10); return p; } int main() { // 在堆区开辟数据 int *p = fun(); cout << *p << endl; cout << *p << endl; cout << *p << endl; delete p; return 0; } 内存分区的意义:
赋予不同区域存储的数据不同的生命周期。
1.2 new操作符
在堆区使用new开辟的内存,在对象销毁前,使用delete释放。
new创建的数据,会返回该数据对应的类型的指针。
创建一个变量 new int(10);
创建一个数组 new int[10];
#include
using namespace std; int *func01() { // 在堆区创建整型数据 // new 返回的是该类型对应的类型的指针 int *p = new int(10); // 开辟内存 return p; } void test01() { int *p = func01(); cout << *p << endl; delete p; // 程序员释放内存 } void func02() { // 数组 int *arr_p = new int[10]; // 10代表数组有10个元素 for (int i = 0; i < 10; i++) { arr_p[i] = i; } for (int j = 0; j < 10; j++) { cout << arr_p[j] << endl; } // 释放堆区的数组 delete[] arr_p; } int main() { test01(); func02(); return 0; } 1.3 new和malloc
new 和 malloc() 是 C++ 和 C 语言中用于申请动态内存的两种方法。尽管它们都提供了从堆中分配内存的机制,但在使用和功能上有一些重要区别:
1). 构造函数和析构函数:
-
new: 不仅分配内存,还会调用对象的构造函数,这意味着分配后立即初始化了对象。当使用 delete 释放内存时,会自动调用析构函数。
-
malloc(): 只分配所需内存大小的字节,不调用构造函数,分配的内存用于原始字节数据。使用 free() 释放内存时,不会调用析构函数。
2). 内存分配失败时的行为:
-
new: 如果无法分配内存,会抛出一个 std::bad_alloc 异常。
-
malloc(): 分配失败时返回 NULL。
3). 内存分配的灵活性:
-
new: 是 C++ 操作符,支持自定义的分配器,可以重载以提供特殊行为。
-
malloc(): 是 C 语言的标准库函数,没有这种灵活性,但由于其简单性,在需要与 C 语言库或旧代码兼容时非常有用。
4). 类型安全:
-
new: 为类型安全,返回正确类型的指针,无需类型转换。
-
malloc(): 返回 void*,通常需要强制类型转换以匹配指针类型。
5). 内存大小:
-
new: 自动计算要分配的对象的大小。
-
malloc(): 需要程序员显式提供字节数。
6). 用法:
-
new: 直接用于对象的创建。
MyClass* obj = new MyClass();
-
malloc(): 需要在分配内存后,可能还需要进行类型转换。
MyClass* obj = (MyClass*)malloc(sizeof(MyClass));
7). 释放内存:
-
对于 new,应使用 delete 来释放内存。
-
对于 malloc(),应使用 free() 来释放内存。
1.4 C++的对象和内存管理 特性
C++ 是一种面向对象的语言,它提供了一系列特性来实现对象的封装、继承和多态。同时,C++ 也提供了灵活而复杂的内存管理功能。以下是一些关键的对象和内存管理特性:
1. 构造函数和析构函数:
-
构造函数:当一个对象被创建时,构造函数用来初始化该对象的状态。用户可以重载构造函数,提供不同方式的初始化。
-
析构函数:当一个对象不再使用时,析构函数被调用,用来执行任何必要的清理工作,比如释放分配的内存等。这是 RAII(资源获取即初始化)的基础,确保在分配资源后,资源在不再需要时能被正确释放。
2. 动态内存管理:
-
C++ 提供了 new 和 delete 操作符来分配和释放对象的内存。与 C 语言的 malloc 和 free 不同,new 和 delete 会调用对象的构造函数和析构函数。
-
智能指针(如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr)是现代 C++(C++11 及以后)中推荐的管理动态分配内存的方式,可以减少内存泄漏和管理责任。
3. 拷贝构造函数和拷贝赋值操作符:
- 用于控制对象的拷贝行为,特别是涉及到动态内存分配的对象。用户可以定义自己的拷贝构造函数和拷贝赋值操作符,以实现深拷贝或禁止对象拷贝等行为。
4. 移动构造函数和移动赋值操作符**(C++11 及以后)**:
- 这是现代 C++ 中的新特性,允许程序“移动”资源,而非传统的拷贝,可以提高资源管理的效率,特别是对于那些分配了大量内存或占用了其他重要资源的对象。
5. 继承和多态:
-
继承允许创建基于现有类的新类,继承现有类的属性和方法。
-
多态允许以统一的方式处理不同类的对象。在 C++ 中,多态通过虚函数(virtual functions)实现,允许在基类的层面定义接口,在派生类中实现具体行为。
6. 封装:
- 封装是面向对象编程的核心概念之一,允许将数据(属性)和用于操作这些数据的代码(方法)绑定到一起作为一个单元(对象),并控制对这些数据的访问。
7. 异常处理:
- C++ 提供了异常处理机制,允许程序在发生错误时进行有效的资源清理。这在内存和资源管理中尤为重要,因为在没有异常处理的情况下,发生错误可能会导致资源泄漏。
2. 函数提高
2.1 函数的默认参数
int fuc(int a = 10, int b = 20, int c = 1) { }
注意:
- 函数声明中有默认参数,函数中就不能有了
- 默认参数前面的形参有了,后面的形参就必须有
2.2 函数的占位参数
函数中有占位参数,在调用的时候,必须填补该位置
void func(int a, int) { } int main() { int a = 10, b = 20; func(a, b); }
2.3 函数重载
1、重载需要满足的三个条件:
- 同一作用域
- 函数名称相同
- 函数的参数类型,参数个数,顺序不同
注意:返回值不可以作为函数重载的条件
void func() { cout << “无参数” << endl; } void func(int a) { cout<< “有参数”<< endl; } int main() { func(); int a = 10; func(a); }
- 注意事项
- 引用作为函数重载的条件
#include
using namespace std; // 引用作为 函数重载 的条件 void test01(int &a) { cout << "int &a" << endl; } void test01(const int &a) // 这块相当于 const int &a = 12; // 这句代码是合法的。 { cout << "const int &a" << endl; } int main() { // int a = 10; // test01(a); // 调用的是 void test01(int &a) test01(12); // 调用的是 void test01(const int &a) return 0; } 2)函数重载遇到默认参数时,会出现二义性。报错
void func(int &a, int b = 10){} void func(int &a){}
小结
C++中在程序运行前分为全局区和代码区
代码区的特点是共享和只读
全局区中存放全局变量、静态变量、常量
常量区中存放const修饰的全局常量和字符串常量
3. 类&对象
1、类的定义:
class classname { Access specifiers: // 访问修饰符:private/public/protected // 成员变量(属性) // 成员方法(行为) }; // 使用“;”分号结束一个类
2、对象的定义
classname Box1; classname Box2; // 声明Box2,类型为classname
3、访问数据
使用"."直接成员访问运算符来访问
#include
using namespace std; class Student { public: // 成员属性 成员变量 string name; int num; public: // 成员方法 成员函数 void fuzhi(string stu_name, int stu_num) { name = stu_name; num = stu_num; } void printInfo() { cout << name << endl; cout << num << endl; } }; int main() { Student stu1; stu1.fuzhi("张三", 2032); stu1.printInfo(); } 3.1 封装(类)
3.1.1 封装的意义
1)将属性和行为作为一个整体,表现生活中的事物 2)将属性和行为加以权限控制
封装的意义一
设计类的时候,属性和行为写在一起,表现事物
封装的意义二
三种访问权限(类访问修饰符)
1、pubic 公共权限 类内可以访问 类外可以访问
2、protected 保护权限 类内可以访问 类外不可以访问
3、private 私有权限 类内可以访问 类外不可以访问
protected和private的区别在继承上:
protected修饰父类的属性和方法 子类可以继承父类的所有属性和方法
private修饰父类的属性和方法 子类不可以继承父类的private修饰的属性和方法
#include
using namespace std; #include // 访问权限 class Persion { public: string f_name; protected: string f_Car; private: int f_Password; public: void func() { f_name = "张三"; f_Car = "拖拉机"; f_Password = 123455; } }; int main() { Persion p1; p1.f_name = "王五"; // p1.f_Car = "宝马"; // 保护权限的属性在所声明类外不能访问 // p1.fPa_ssword = 314124; // 私有属性在所声明类外不能访问 } 3.1.2 struct和class的区别
struct(结构体)默认访问权限是 公共public
class (类)默认访问权限是 私有private
3.1.3 成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,可以检测数据的有效性。
#include
using namespace std; #include class Person { public: void setName(string name) // 设置m\_name的可写权限 { m_name = name; } string getName() // 可读F { return m_name; } int getAge() // 可读 { return m_age; } void setLover(string lover) { m_Lover = lover; } private: string m_name; // 可读可写 int m_age; // 只读权限 string m_Lover; // 只写权限 }; int main() { Person p1; p1.setName("张三"); cout << "m_name = " << p1.getName() << endl; cout << "m_age = " << p1.getAge() << endl; p1.setLover("王女士"); } 3.2 对象的初始化和清理
当我们没有定义任何构造函数时,编译器会自动定义一个(隐含的)默认构造函数,这个默认构造有时候负责基类的构造和成员对象的构造(在组合和继承中有体现)
3.2.1 构造函数&析构函数
类似于生活中的初始化(构造函数)和清理(析构函数)
构造函数和析构函数由编译器自动调用,无需手动调用。
作用:
构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:在对象销毁前系统自动调用,执行一些清理工作。
1. 构造函数
特点:
1)构造函数名和类名相同。
2)没有返回值,也不会返回void。
3)构造函数可以有参数,因此可以发生重载。
4)程序在调用对象时会自动调用构造,无需手动调用,而且只会调用一次。
#include
using namespace std; class Line { private: double length; public: // 成员函数声明 void setLength(double len); double getLength(); Line(double len); // 带参数的构造函数 }; void Line::setLength(double len) { length = len; } double Line::getLength() { return length; } // 构造函数 Line::Line(double len) { cout << "对象被创建时,线的长度为:" << len << endl; length = len; } int main() { Line l1(10.0); cout << "length的值为:" << l1.getLength() << endl; // 默认值大小 l1.setLength(12.0); cout << "length的值为:" << l1.getLength() << endl; // 再次设置后的大小 return 0; } 2. 析构函数
作用:将我们堆区开辟的内存释放掉
特点:
1)析构函数名与类名相同,前加“~”作为前缀。
2)没有返回值,也不带参数。
3)不可以有参数,因此不能发生重载。
4)程序在对象销毁前会自动调用析构 ,无须手动调用,而且只会调用一次。
#include
using namespace std; class Line { private: double length; public: void setLength(double len); double getLength(); Line(); // 声明构造函数 ~Line(); // 声明析构函数 }; void Line::setLength(double len) { length = len; } double Line::getLength() { return length; } // 定义构造函数 Line::Line() { length = 10.0; cout << "创建对象时,初始化的 length = " << length << endl; } // 定义析构函数 Line::~Line() { cout << "函数运行结束前执行函数(析构函数)" << endl; } int main() { Line l1; cout << "构造函数初始化的成员属性length值: " << l1.getLength() << endl; l1.setLength(11.0); cout << "重新设定的成员属性length值: " << l1.getLength() << endl; } 3.2.2 构造函数的分类及调用(拷贝构造)
1. 构造函数按照类型分为
-
普通构造函数
默认构造函数
-
委托构造函数
Clock(int newH, int newM, int news) // 构造函数 { hour = newH; minute = newM; second = news; } Clock():Clock(0, 0, 0){} // 委托构造函数
-
拷贝(复制)构造函数:
将一个对象的所有属性拷贝到另一个相同类的对象身上。
用一个已经存在的对象(复制构造函数的参数确定)去初始化同类的一个新对象。
如果没有定义类的复制构造函数,系统会在必要的时候自动生成一个隐含的复制构造函数。
#include
using namespace std; /* 拷贝构造函数 */ class Point { public: Point(int xx, int yy) { x = xx; y = yy; } Point(Point &p) // 把a对象赋值给p对象 { x = p.x; y = p.y; std::cout << "Calling the copy constructor" << std::endl; } int getX() { return x; } int getY() { return y; } private: int x, y; }; void func1(Point p) // 把b对象赋值给p对象 { cout << p.getX() << endl; } Point func2() { Point a(1, 2); return a; // 局部对象赋值给全局对象 } int main(int argc, char const *argv[]) { Point a(1, 20); Point b(a); // 1. 用a初始化b,调用拷贝构造函数 Point c = a; func1(b); // 2. 对象b作为func1()的实参 b = func2(); // 3. func2()返回类对象 cout << b.getX() << "; " << b.getY() << endl; return 0; } -
移动构造函数
左值:对象的身份;
右值:对象的值。
左值持久,右值短暂(右值要么是字面常量,要么是表达式求值过程中创建的临时变量)
左值引用:通过&获得左值引用
右值引用:必须绑定到右值的引用(只能绑定到一个将要销毁的对象),实际上就是某个对象的另一个名字。通过&&获得右值引用。
int i = 20; int &r = i; // r引用i(r是左值引用) int &&rr = i * 40; // rr引用i \* 40(rr是右值引用) const int &r2 = i * 40; // 可以将一个const引用绑定到一个右值上(将r2绑定到i \* 40上)
note! 因为变量是左值,所以不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用也不行。
2. 构造函数按照参数分类
- 有参构造
- 无参构造(默认的构造,默认构造)
3. 三种调用方法
- 括号法
- 显示法
- 隐式转换法
#include
using namespace std; class Person { public: Person() // 构造函数 无参构造 { cout << "类的 无参构造 函数,创建对象时执行!!!" << endl; } Person(int a) // 有参构造 { age = a; cout << "类的 有参构造 函数" << endl; } Person(const Person &p) // 拷贝构造** 使用const限定,不能把本体给修改了 { // 将传入的p对象(人)身上的所有属性,拷贝到我身上 age = p.age; } ~Person() // 析构函数 { cout << "类的析构函数,在程序运行结束前执行" << endl; } int age; }; // 调用 void test01() { // 1、括号法 Person p1; // 无参构造调用 Person p2(10); Person p3(p2); // 拷贝构造函数的调用 cout << "p2.age = " << p2.age << endl; cout << "p3.age = " << p3.age << endl; // 2、显示法 Person p6 = Person(); Person p4 = Person(12); Person p5 = Person(p4); // 3、隐式转换 Person p7 = 13; Person p8 = p7; } int main() { test01(); return 0; } 3.2.3 拷贝构造函数/调用时机
使用同一类中之前创建的对象来初始化新对象。
拷贝构造函数的定义
className(const className &obj){}
C++中拷贝构造函数调用的时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象;
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
3.2.4 深拷贝与浅拷贝
经典的面试问题
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作。
3.2.5 使用初始化列表初始化字段
假设一个类C有多个字段X,Y,Z等需要进行初始化
class C { public: double X, Y, Z; C(double X, doubleY, double Z); }; C::C(double X, double Y, double Z): X(a), Y(b), Z(c) {}
3.2.6 类对象作为类成员(组合类)
C++类中的成员可以是另一个类的对象,称该成员为 对象成员。
class A {}; class B { A a; // A是B类中的对象成员。 };
问题:当创建B对象的时候,A/B的构造和析构顺序是怎样的?
#include
#include using namespace std; class Phone { public: string m_PName; Phone(string pName) { m_PName = pName; cout << "调用Phone类的构造函数" << endl; } ~Phone() { cout << "调用Phone类的析构函数" << endl; } }; class Person { public: string m_Name; Phone m_Phone; // Phone m_Phone = pName; 隐式转换法 Person(string name, string pName) : m_Name(name), m_Phone(pName) { cout << "调用Person类的构造函数" << endl; } ~Person() { cout << "调用Person类的析构函数" << endl; } }; int main(int argc, char const *argv[]) { // 当其他类对象作为本类成员,构造的时候先构造类对象,再构造自身 // 构造的顺序和析构的顺序是相反的 Person p("张三", "苹果MAX"); return 0; } 3.2.7 静态成员
静态成员属于类本身,不属于某个实例。
“成员”出现在类中,在类中无论是函数,还是变量都称为成员——成员函数/成员变量
静态成员:在成员变量/成员函数前加上关键字static,称为静态成员。
静态成员分为:
1. 静态成员变量
特点:
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化
#include
using namespace std; class Person { public: // 1. 所有对象都共享同一份数据 // 2. 编译阶段就分配内 // 3. 类内声明,类外初始化 static int m_A; Person(/* args */); ~Person(); private: // 静态成员变量也有访问权限 私有权限在类外访问不到 // 类内声明 static int m_B; }; // 类外初始化 int Person::m_A = 100; // 意思是 Person这个类的作用域下的m\_A静态成员 int Person::m_B = 400; Person::Person(/* args */) // 意思是 Person这个类的作用域下的Person构造函数 { } Person::~Person() // 意思是 Person这个类的作用域下的Person析构函数 { } // 所有对象共享同一份数据 void test01() { Person p; cout << p.m_A << endl; // 100 Person p2; p2.m_A = 200; cout << p.m_A << endl; // 200 } void test02() { // 静态成员变量 不属于某个对象上,多有对象共享同一份数据 // 因此静态成员变量有两种访问方式 // 1. 通过对象进行访问 Person p; cout << "通过对象访问静态变量:" << p.m\_A << endl; // 100 // 2. 通过类名进行访问 cout << "通过类名访问静态变量:" << Person::m\_A << endl; // 100 } int main(int argc, char const *argv[]) { // test01(); test02(); } 2. 静态成员函数
特点:
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
需要注意的!!!
静态成员函数的两种调用方式;
静态成员函数的访问权限;
#include
using namespace std; class Person { public: static void func() { m_A = 200; // 静态成员函数可以访问 静态成员变量 // m_B = 100; // 静态成员函数不可以访问,非静态成员变量(静态成员变量不属于某一个对象,非静态成员变量属于某个对象 无法区分是哪个对象的m\_B 因此不能访问) cout << "静态成员函数" << endl; } static int m_A; // 静态成员变量 int m_B; // 非静态成员变量 Person(/* args */){}; ~Person(){}; // 静态成员函数也是有访问权限的 private: static void func1() { cout << "私有的静态成员函数" << endl; } }; int Person::m_A = 0; void test01() { // 静态成员函数的2种访问方式 // 1.通过对象访问 Person p; p.func(); // 2.通过类名访问(因为不属于某个对象,因此可以通过类名访问) Person::func(); // 静态成员函数也是有访问权限的 // Person::func1(); // 类外不能访问私有静态成员函数 } int main(int argc, char const *argv[]) { test01(); return 0; } 3.3 C++对象模型和this指针
3.3.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
#include
using namespace std; class Person { public: int m_A; // 非静态成员变量, 属于类的对象上。 static double m_B; // 静态成员变量,不属于类的某一个对象。 void func(){}; // 非静态成员函数 不属于类的对象上 static void func2(){}; // 静态成员函数 不属于类的对象上 }; double Person::m_B = 0; void test01() { Person p; // 空对象占用内存空间为: // C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占用内存的位置 // 每个空对象也应该有一个独一无二的位置 cout << "Person p这个空对象,占用内存空间:" << sizeof(p) << endl; // 1 } void test02() { Person p; // 因为非静态成员变量属于类的某一个对象,而静态成员变量不属于某一个对象,因此在输入对象的大小的时候,只有非静态成员变量的大小。 // 只有非静态成员变量属于类上,其他都不属于类上。 cout << "sizeof : " << sizeof(p) << endl; // 4 } int main(int argc, char const *argv[]) { // test01(); test02(); return 0; } 3.3.2 this指针的概念
每一个非静态成员函数只会诞生一份函数实例,即多个不同类型的对象会共用一块代码。
这一块代码如何区分是哪个对象调用自己的?
通过this指针。this指针 指向被调用的成员函数 所属的对象。
this指针 是 隐含在每一个非静态成员函数内 的指针。
this指针 不需要定义,直接使用即可。
this指针的用途:
-
当形参和成员函数内的变量同名时,可以使用this区分;
-
在类的非静态成员函数中返回对象本身,可使用return *this;
#include
using namespace std; class Person { public: int age; void func(int age) { // age = age; // 这里第一个age和第二个age未进行区分 // this指针 指向 p1 这个对象(调用这个函数的对象) this->age = age; } Person &PersonAddAge(Person &p) { this->age += p.age; // this是指向 p2 对象的一个指针 return *this; // * 解引用。即代表p2对象。 } }; // 1. 解决名称冲突 void test() { Person p1; p1.func(18); cout << "p1.age : " << p1.age << endl; } // 2. 返回对象本身 void test02() { Person p1; Person p2; p1.age = 11; p2.age = 10; // 链式编程思想 p2.PersonAddAge(p1).PersonAddAge(p1); cout << "p2的年龄是: " << p2.age << endl; } int main(int argc, char const *argv[]) { // test(); test02(); return 0; } 3.3.3 空指针访问成员函数
C++中空指针可以调用成员函数,但是需要注意是否用到this指针
如果用到this指针,需要判断保证代码的健壮性。
#include
using namespace std; class Person { public: void showClassName() { cout << "Person 类" << endl; } void showPersonAge() { // 报错是因为传入的指针为空。this对象为空,不存在m_Age成员属性。 if (this == NULL) { return; } cout << "age = " << this->m_Age << endl; // this代表当前对象。通过当前对象访问属性。 } int m_Age; }; void test() { Person *p = NULL; // 空指针是可以访问成员的 p->showClassName(); p->showPersonAge(); } int main(int argc, char const *argv[]) { test(); return 0; } 3.3.4 const修饰成员函数
const修饰后,限制为只读状态
常函数:
- 成员函数加const 常函数
- 常函数中内不可以修改成员属性
- 成员属性声明时加上mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const 常对象
- 常对象只能调用常函数
#include
using namespace std; class Person { public: // this指针的本质 是指针常量 指针的指向不可以修改 // Person *const this; // this指向了一个Person void showPerson() const // 这里的const修饰的是this指针 // 这里的const相当于 const Person *this; 使得指针指向的值不可以修改。 // 两者综合起来就是const Person *const this; { // this->m_A = 100; // 不可以修改指针指向的值,因为函数名后的const修饰了this指针。 // this = NULL; // this指针不可以修改指针的指向 this->m_B = 100; // 加mutable关键字后,可以修改。 } void func() { m_A = 100; } Person(); int m_A; mutable int m_B; }; Person::Person() { cout << "Person类的构造函数" << endl; } // 常成员函数 void test01() { Person p; p.showPerson(); } // 常对象 void test02() { const Person p; // 对象前加const 常对象 // p.m_A = 100; p.m_B = 100; // m_B是特殊值,在常对象下也可以修改 // 常对象只能调用常函数 p.showPerson(); // p.func(); } int main(int argc, char const *argv[]) { test02(); return 0; } 3.4 友元
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要使用友元技术
友元的目的:让一个函数或者类访问另一个类中的私有成员
友元的关键字:friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
3.4.1 全局函数做友元
#include
using namespace std; #include // 建筑物类 class Building { // 说明全局函数goodGay是Building类的友元,可以对类中的私有成员访问 friend void goodGay(Building *buiding); public: Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } string m_SittingRoom; // 客厅 private: string m_BedRoom; // 卧室 }; // 全局函数 void goodGay(Building *buiding) { cout << "好朋友正在访问: " << buiding->m_SittingRoom << endl; cout << "好朋友正在访问: " << buiding->m_BedRoom << endl; } void test01() { Building b; goodGay(&b); } int main(int argc, char const *argv[]) { test01(); return 0; } 3.4.2 类做友元
#include
#include using namespace std; class Buiding; class GoodGay { public: GoodGay(); void visit(); // 参观函数 访问buiding中属性 Buiding *buiding; }; class Buiding { // 告诉编译器 GoodGay是Buiding的友元,可以访问Buiding的私有成员属性 friend class GoodGay; public: Buiding(); string m_SittingRoom; private: string m_BedRoom; }; Buiding::Buiding() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } GoodGay::GoodGay() { buiding = new Buiding; } void GoodGay::visit() { cout << "好朋友正在访问: " << buiding->m_SittingRoom << endl; cout << "好朋友正在访问: " << buiding->m_BedRoom << endl; } void test01() { GoodGay gg; gg.visit(); } int main(int argc, char const *argv[]) { test01(); return 0; } 3.4.3 成员函数做友元
#include
#include using namespace std; class Buiding; class GoodGay { public: GoodGay(); Buiding *buiding; void visit01(); // 成员函数做友元 void visit02(); }; class Buiding { // 告诉编译器GoodGay::visit01()是Buiding的友元,可以访问私有成员属性 friend void GoodGay::visit01(); public: Buiding(); string m_SittingRoom; private: string m_BedRoom; }; GoodGay::GoodGay() { buiding = new Buiding; } Buiding::Buiding() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } void GoodGay::visit01() { cout << "visit01 成员函数正在访问: " << buiding->m_SittingRoom << endl; cout << "visit01 成员函数正在访问: " << buiding->m_BedRoom << endl; } void GoodGay::visit02() { cout << "visit02 成员函数正在访问: " << buiding->m_SittingRoom << endl; } void test01() { GoodGay gg; gg.visit01(); gg.visit02(); } int main(int argc, char const *argv[]) { test01(); return 0; } 3.4 运算符重载
对已有的运算符重新定义,赋予其另一种功能,以适应不同的数据类型。
3.4.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算
通过自己写成员函数,实现两个对象属性相加后返回新的对象。
#include
using namespace std; class Person { public: int m_A; int m_B; /* // 自己写成员函数,实现两个对象属性相加后返回新对象 Person PersonAddPerson(Person &p) { Person temp; temp.m_A = this->m_A + p.m_A; temp.m_B = this->m_B + p.m_B; return temp; } */ // 通过成员函数重载+号运算符 Person operator+(Person &p) { Person temp; temp.m_A = this->m_A + p.m_A; temp.m_B = this->m_B + p.m_B; return temp; } }; void test() { Person p1; p1.m_A = 10; p1.m_B = 11; Person p2; p2.m_A = 12; p2.m_B = 13; // 1. 通过调用自己写的成员函数计算 // Person p3 = p1.PersonAddPerson(p2); // 2. 通过成员函数重载+号运算符 Person p3 = p1.operator+(p2); // 未进行简化的写法 // Person p3 = p1 + p2; // 简化后的写法 printf("m_A: %d, m_B: %d", p3.m_A, p3.m_B); } int main(int argc, char const *argv[]) { test(); return 0; } 例二:
#include
using namespace std; class Person { public: int m_A; int m_B; }; Person operator+(Person &p1, Person &p2) { Person temp; temp.m_A = p1.m_A + p2.m_A; temp.m_B = p1.m_B + p2.m_B; return temp; } // 函数重载版本 Person operator+(Person &p1, int num) { Person temp; temp.m_A = p1.m_A + num; temp.m_B = p1.m_B + num; return temp; } void test() { Person p1; p1.m_A = 10; p1.m_B = 11; Person p2; p2.m_A = 12; p2.m_B = 13; // 通过 全局函数重载+号运算符 // 1. 全局函数重载本质调用 // Person p3 = operator+(p1, p2); // 2. 简化后的写法 // Person p3 = p1 + p2; // 运算符重载 也可以发生函数重载 // Person p3 = operator+(p1, 10); Person p3 = p1 + 10; printf("m_A: %d, m_B: %d", p3.m_A, p3.m_B); } int main(int argc, char const *argv[]) { test(); return 0; } 3.4.2 左移运算符重载
重载的两种形式:1)通过成员函数重载,2)通过全局函数重载。
链式编程思想:调用完成之后,返回cout;再次调用完成,返回cout;
#include
using namespace std; class Person { public: int m_A; int m_B; // 利用成员函数重载 左移运算符 p.operator<<(ostream cout) 简化版本p << cout // 不能利用成员函数重载 <<运算符,因为无法实现 cout在左侧 // void operator<<(cout){} }; // 只能使用全局函数进行重载 <<运算符 ostream &operator<<(ostream &cout, Person &p) // 本质 operator<<(cout, p) 简化 cout< 3.4.3 递增运算符重载
3.4.4 赋值运算符重载
3.4.5 关系运算符重载
3.4.6 函数调用运算符重载
类成员函数
类成员函数是类的一个成员,可以操作类中的所有成员。使用成员函数来访问类的成员,而不是直接访问这些类的成员。
成员函数可以定义在:
1、类的内部
class Box { public: double length; double breadth; double height; // 定义在类内部的成员函数 double getVolume() { return length * breadth * height; } };
2、类的外部(即单独定义)
使用范围解析运算符“::”定义
#include
using namespace std; class Box { public: double length; double breadth; double height; // 声明成员函数 需要在类的内部声明定义在类的外部的成员函数 double getVolume(); void setLength(double len); void setBreadth(double bre); void setHeight(double hei); }; // 定义在类外部的 成员函数 // 需要使用范围解析运算符“::” double Box::getVolume(void) // 即在Box类的作用域下的getVolume函数 { return length * breadth * height; } void Box::setLength(double len) { length = len; } void Box::setBreadth(double bre) { breadth = bre; } void Box::setHeight(double hei) { height = hei; } int main() { Box b1; b1.setBreadth(12.0); b1.setHeight(32.0); b1.setLength(78.0); cout << "b1 volume: " << b1.getVolume() << endl; return 0; } 二、继承
基类&派生类
基类(父类)
派生类(子类)
继承是 is a的关系。
继承类型(继承方式):当一个类派生自基类,这个基类可以被修饰为public/protected/private
当这个基类被
public修饰时:
protected修饰时:当一个类派生自protected基类时,这个基类的public/protected成员将成为这个派生类的protected成员。
private修饰时:派生类继承自基类的所有属性和方法都成为派生类的private属性和方法(成员变量/成员方法)
多继承
虚函数和纯虚函数
虚函数:
引入原因:允许使用基类的指针调用子类的这个函数。
定义一个函数为虚函数,不代表这个函数没有被实现(可以有实现)。
#include
using namespace std; class A { public: virtual void foo() { cout << "基类的虚函数" << endl; } }; class B : public A { public: void foo() { cout << "派生类 B类" << endl; } }; int main() { A *a = new B(); a->foo(); // 这里的指针指向的是基类A,调用的成员函数foo()是派生类B的成员函数 return 0; } /*
派生类 B类
*/
纯虚函数:
纯虚函数是在基类中被声明的虚函数,没有实现,
但是要求所有派生类都需要有自己的实现方法。
在基类中实现纯虚函数,即:函数原型后加=0
virtual void function()=0;
特点:
函数没有被实现。
引入原因:
-
方便使用多态特性
-
基类本身生成对象是不合理的。
-
需要在基类中定义虚函数,然后在派生类中重新定义这个函数,但是在基类中又不能给出有意义的实现,这个时候可以用到纯虚函数。
virtual int a() = 0;
-
定义一个函数为纯虚函数,是为了实现一个接口,起到规范作用。
#include
using namespace std; class Animal { protected: int age = 0; int height = 0; int weight = 0; public: Animal() { cout << "构造函数" << endl; } virtual ~Animal() // 防止通过基类的指针删除派生类对象时,派生类的析构函数不能被正确的调用 { cout << "析构函数" << endl; } virtual void Walk() = 0; virtual void Eat() = 0; virtual void Sleep() = 0; }; class Dog : public Animal { public: void Walk() { cout << "Dog Walk()方法" << endl; } void Eat() { cout << "Dog Eat()方法" << endl; } void Sleep() { cout << "Dog Sleep()方法" << endl; } }; class Cat : public Animal { public: void Walk() { cout << "Cat Walk()方法" << endl; } void Eat() { cout << "Cat Eat()方法" << endl; } void Sleep() { cout << "Cat Sleep()方法" << endl; } }; int main() { Animal *dog1 = new Dog(); Animal *cat1 = new Cat(); dog1->Eat(); dog1->Sleep(); dog1->Walk(); cat1->Eat(); cat1->Sleep(); cat1->Walk(); delete cat1; delete dog1; /* 当两个指针被delete时,首先调用的是Dog/Cat类的析构函数,然后是Base类(Animal)的析构函数。这是因为基类的析构函数被声明为虚。 */ return 0; } 抽象类
带有纯虚函数的类叫做抽象类。
设计抽象类的目的:给其他类提供一个适当的可以继承的基类。
三、多态
父类中同一个方法,在继承的子类中表现出不同的形式。重写和重载
C++提高编程
C++中除了有面向对象的编程思想还有泛型编程的思想
1. 模板
学习模板是为了使用STL中的模板,会使用其中的模板
1.1 函数模板
C++中的另一种编程思想:泛型编程,主要利用的技术是模板
C++中的两种模板机制:函数模板 类模板
1.1.1 函数模板语法
函数模板作用:
建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个虚拟的类型来代表。在使用的时候确定。
语法:
template
函数声明和定义
解释:
template 声明创建模板
typename 表明其后面的符号是一种数据类型,可以用class代替。不管是函数模板,还是类模板都可以使用class/typename都可以
T 通用的数据类型,名称可以替换,通常为大写字母
#include
using namespace std; void swapInt(int &a, int &b) { int tmp = a; a = b; b = tmp; } void swapDouble(double &a, double &b) { double tmp = a; a = b; b = tmp; } // 函数模板 template // 声明一个模板,告诉编译器后面代码中紧跟着的T不要报错,T是一个通用数据类型 void mySwap(T &a, T &b) { T tmp = a; a = b; b = tmp; } void test01() { int a = 10; int b = 20; // swapInt(a, b); double c = 1.1; double d = 2.2; // swapDouble(c, d); // 使用函数模板交换 // 两种方式使用函数模板 // 1. 自动类型推导 // mySwap(a, b); // mySwap(c, d); // 2. 显示指定类型 mySwap (a, b); mySwap (c, d); cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; cout << "d = " << d << endl; } int main(int argc, char const *argv[]) { test01(); return 0; } 函数模板
template
T Add(T a, T b) { return a + b; } int main(){ cout << Add (1, 2) << endl; cout << Add (1.5, 2.5) << endl; cout << Add (“Hello, ”, “World!”)< inline T const& Max (T const& a, T const& b) { return a < b ? b:a; } // 定义了一个模板函数“Max”,可以接受两个T类型(任意类型)的常量引用参数。 // inline表示是一个内联函数。内联函数的代码在调用时会被直接嵌入到调用的代码中 1.1.2 函数模板注意事项
注意事项:
· 自动类型推导,必须推导出一致的数据类型T,才可以使用;
· 模板必须要确定出T的数据类型,才可以使用。
#include
using namespace std; template void mySwap(T &a, T &b) { T tmp = a; a = b; b = tmp; } void test() { int a = 10; int b = 20; char c = 'c'; // 注意事项1. 自动类型推导,必须推导出一致的数据类型T,才可以使用 mySwap(a, b); // 正确 // mySwap(a, c); // 错误 cout << "a = " << a << endl; cout << "b = " << b << endl; } template void func() { cout << "func调用" << endl; } void test02() { // 注意事项2. 模板必须要确定出T的数据类型,才可以使用。 // func(); func (); } int main(int argc, char const *argv[]) { test(); // test02(); return 0; } 1.1.3 函数模板案例----数组排序
#include
using namespace std; // 实现通用 对数组进行排序的函数 // 规则 从大到小 // 算法 选择 // 测试 char数组、int数组 // 交换函数模板 template void mySwap(T &a, T &b) { T temp = a; a = b; b = temp; } // 排序算法 template void mySort(T arry[], int len) { for (int i = 0; i < len; i++) { int max = i; // 认定的最大值下标 for (int j = i + 1; j < len; j++) { // 认定的最大值 比 遍历出的数值 要小,说明j下表的元素才是真正的最大值 if (arry[max] < arry[j]) { max = j; // 更新下标 } } if (max != i) // 认定的最大值i和计算出的最大值max不相等 { // 交换max和i下标的元素 mySwap(arry[max], arry[i]); } } } // 打印数组模板函数 template void printArray(T arr[], int len) { for (int i = 0; i < len; i++) { cout << arr[i] << " "; } cout << endl; } void test01() { char arr[] = "badcfe"; int len = sizeof(arr) / sizeof(char); mySort(arr, len); printArray(arr, len); } void test02() { int arr[] = {8, 2, 3, 1, 9, 10}; int len = sizeof(arr) / sizeof(int); mySort(arr, len); printArray(arr, len); } int main() { test01(); test02(); return 0; } 1.1.4 普通函数与函数模板的区别
普通函数与函数模板的区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
#include
using namespace std; // 普通函数与函数模板的区别 // 普通函数 int myAdd01(int a, int b) { return a + b; } // 函数模板 template T myAdd02(T a, T b) { return a + b; } void test01() { int a = 10; int b = 20; char c = 'c'; cout << myAdd01(a, b) << endl; // 30 cout << myAdd01(a, c) << endl; // 隐式类型转换 109 // 自动类型推导 调用函数模板 cout << myAdd02(a, b) << endl; // cout << myAdd02(a, c) << endl; // 报错,不能进行隐式类型转换 // 显示指定类型 调用函数模板 cout << myAdd02 (a, c) << endl; // 可以进行隐式类型转换 } int main(int argc, char const *argv[]) { test01(); return 0; } 1.1.5 普通函数与函数模板的调用规则
调用规则:
1. 如果函数模板和普通函数都可以实现,优先调用普通函数;
2. 可以通过空模板参数列表来强制调用函数模板;
3. 函数模板也可以发生重载;
4. 如果函数模板可以产生更好的匹配,优先调用函数模板。
#include
using namespace std; void myPrint(int a, int b) { cout << "调用普通函数" << endl; } template void myPrint(T a, T b) { cout << "调用函数模板" << endl; } template void myPrint(T a, T b, T c) { cout << "调用重载的函数模板" << endl; } void test01() { int a = 10; int b = 20; int c = 30; // myPrint(a, b); // 这里调用的是普通函数 // 通过空模板参数列表,强制调用函数模板 // myPrint<>(a, b); // // 函数重载 // myPrint(a, b, c); // 匹配最优 char c1 = 'a'; char c2 = 'b'; myPrint(c1, c2); } int main(int argc, char const *argv[]) { test01(); return 0; } 1.1.6 模板的局限性
template
void f(T a, T b) { a = b; } 如果传入的a和b是数组则不成立;
template
void f(T a, T b) { if(a > b){….} } 如果T类型传入的是Person这样的自定义数据类型(编译器不能辨别这种类型),无法正常运行;
因此提供模板的重载,为这些特定的类型提供具体化的模板
#include
using namespace std; #include class Person { public: Person(string name, int age) { (*this).m_Name = name; this->m_Age = age; } string m_Name; int m_Age; }; // 对比两个数据是否相等函数 template bool myCompare(T &a, T &b) { if (a == b) { return true; } else { return false; } } // 利用具体化的Person版本实现代码,具体化优先调用 template <> bool myCompare(Person &p1, Person &p2) { if (p1.m_Age == p2.m_Age && p1.m_Name == p2.m_Name) { 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; } } // 解决方法: // 1、运算符重载 // 2、提供具体的Person版本实现比较 void test02() { // 1、括号法调用构造函数 // Person p1("Tom", 10); // Person p2("Tom", 10); // 2、显式法调用构造函数 Person p1 = Person("Tom", 10); Person p2 = Person("Tom", 10); bool ret = myCompare(p1, p2); if (ret) { cout << "p1 == p2" << endl; } else { cout << "p1 != p2" << endl; } } int main(int argc, char const *argv[]) { // test01(); test02(); return 0; } 1.2 类模板
1.2.1 类模板语法
类模板的作用:
建立一个通用类,类中的成员 数据类型可以不具体指定,用一个虚拟的类型代表。
template
// 或者 template 类
template 声明创建模板
typename/class 表明其后面的符号是一种数据类型
T 通用的数据类型,名称可以替换,通常使用大写字母
#include
using namespace std; #include template class Person { public: Person(AgeType age, NameType name) { this->m_Age = age; this->m_Name = name; } void showPerson() { cout << "name : " << this->m_Name << "; age : " << this->m_Age << endl; } AgeType m_Age; NameType m_Name; }; void test01() { // Person p1 = Person (10, "Tom"); Person p1(10, "Tom"); // <>模板的参数列表 p1.showPerson(); } int main(int argc, char const \*argv[]) { test01(); return 0; } 1.2.2 类模板与函数模板区别
区别有两点:
1. 类模板没有自动类型推导的使用方式;
2. 类模板在模板参数列表中可以有默认参数。
#include
using namespace std; #include template // 类模板中参数列表可以有默认参数 class Person { public: Person(NameType name, AgeType age) { this->m_Name = name; this->m_Age = age; } void showPerson() { cout << "name = " << this->m_Name << "; " << "age = " << this->m_Age << endl; } NameType m_Name; AgeType m_Age; }; void test01() { Person p1("Tom", 10); // 注意!!!! p1.showPerson(); } int main(int argc, char const *argv[]) { test01(); return 0; } 1.2.3 类模板中成员函数创建时机
类模板中成员函数和普通类中的成员函数创建时机不同:
· 普通类中的成员函数一开始就可以创建;
· 类模板中的成员函数在调用时才创建(因为在创建对象时才能确定数据类型)。
1.2.4 类模板对象做函数参数
三种传入方式:
1. 指定传入的类型 ---- 直接显示对象的数据类型
2. 参数模板化 ---- 将对象中的参数变为模板进行传递
3. 整个类模板化 ---- 将这个对象类型模板化进行传递
#include
using namespace std; #include template class Person { public: Person(T1 age, T2 name) { this->m_Age = age; this->m_Name = name; } void showPerson() { cout << "age = " << this->m_Age << "; " << "name = " << this->m_Name << endl; } T1 m_Age; T2 m_Name; }; // 1、指定传入类型 void printPerson1(Person &p) { p.showPerson(); } void test01() { Person p1(10, "Tom"); printPerson1(p1); } // 2、参数模板化 template void printPerson2(Person &p) { p.showPerson(); cout << "T1 类型为:" << typeid(T1).name() << endl; // 查看类型是什么 cout << "T2 类型为:" << typeid(T2).name() << endl; } void test02() { Person p2(20, "Jim"); printPerson2(p2); } // 3、整个类模板化 template void printPerson3(T &p) { p.showPerson(); cout << "T 类型为:" << typeid(T).name() << endl; } void test03() { Person p3(20, "Blue"); printPerson3(p3); } int main(int argc, char const \*argv[]) { // test01(); // test02(); test03(); return 0; } 1.2.5 类模板与继承
1.2.6 类模板成员函数类外实现
1.2.7 类模板分文件编写
1.2.8 类模板与友元
1.2.9 类模板案例
类模板
template
class Stack { private: vector elems; public: void push(T const &); // 这里&后面的参数名称可以省略 void pop(); T top() const; bool empty() const { return elems.empty(); } Stack(); // 声明构造函数 ~Stack(); // 声明析构函数 }; template void Stack ::push(T const &elem) { elems.push_back(elem); } template void Stack ::pop() { if (elems.empty()) { throw out_of_range("Stack<>::pop(): empty stack()"); } elems.pop_back(); } template T Stack ::top() const // const声明常量成员函数,表明top函数不能修改对象的成员变量,即不能修改Stack类中的成员变量。 { if (elems.empty()) { throw out_of_range("Stack<>::top(): empty stack"); } return elems.back(); } template Stack ::Stack() // 构造函数 { printf("构造函数"); } template Stack ::~Stack() // 析构函数 { printf("析构函数"); } int main(int argc, char const \*argv[]) { try { Stack intStack; // 实例化一个Stack类,intStack Stack stringStack; // 操作int类型的Stack intStack.push(7); cout << intStack.top() << endl; cout << "\n" << endl; // 操作string类型的栈 stringStack.push("hello"); cout << stringStack.top() << endl; stringStack.pop(); stringStack.pop(); } catch (const std::exception &e) { std::cerr << "Exception: " << e.what() << '\n'; } return 0; } 2. STL
2.1 STL诞生
· 希望可以建立可重复使用的东西;
· C++的面向对象和泛型编程思想,目的就是提升复用性;
· 大多数情况下,数据结构和算法没有一套标准,导致大量重复工作;
· 为了建立数据结构和算法的一套标准,诞生了STL。
2.2 STL基本概念
·STL(Standard Template Library,标准模板库)
·STL从广义上分为:容器(container)、算法(algorithm)、迭代器(iterator)
·容器和算法直接通过迭代器进行无缝衔接;
·STL几乎所有的代码都采用了模板类或者模板函数。
2.3 STL六大组件
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
1、容器:各种数据结构,如vector、list、deque、set、map等,用来存储数据。
2、算法:各种常用的算法,如sort、find、copy、for_each等。
3、迭代器:扮演容器和算法之间的胶合剂。
4、仿函数:行为类似函数,可作为算法的某种策略。
5、适配器:一种用来修饰容器或者仿函数或者迭代器接口的东西。
6、空间配置器:负责空间的配置与管理。
2.4 STL中容器、算法、迭代器
容器:数据存放的地方
STL容器是将运用最广泛的一些数据结构实现出来
常用的数据结构有:数组、链表、树、栈、队列、集合、映射表等
这些容器分为序列式容器和关联式容器两种:
序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置 关联式容器:二叉树结构,各元素之间没有严格的屋里上的顺序关系
算法:问题的解决办法
算法分为**:质变算法和非质变算法**
**质变算法:**指运算过程中会改变区间内的元素的内容。例如拷贝,替换,删除等
**非质变算法:**运算过程中不会更改区间内的元素的内容。例如查找,计数,遍历,寻找极值等
迭代器:容器和算法之间的粘合剂
提供一种方法,使其能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
每个容器都有自己专属的迭代器。
迭代器使用非常类似于指针。
迭代器的种类:
种类 功能 支持运算 输入迭代器 对数据的只读访问 只读,支持++、==、!= 输出迭代器 对数据的只写访问 只写,支持++ 前向迭代器 读写操作,并能向前推进迭代器 读写,支持++、==、!= 双向迭代器 读写操作,并能向前和向后操作 读写,支持++、– 随机迭代器 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 读写,支持++、–、[n]、-n、<、<=、>、>= 2.5 容器算法迭代器初识
STL中最常用的容器为vector,可以理解为数组
2.5.1 vector存放内置数据类型
容器:vector
算法:for_each
迭代器:vector::iterator
#include
using namespace std; #include #include // 标准算法头文件 // vector容器存放内置数据类型 void printFunc(int val) { cout << val << endl; } void test() { // 创建一个vector容器,数组 vector v; // 向容器中插入数据 v.push_back(10); v.push_back(20); v.push_back(30); v.push_back(40); v.push_back(50); // // 通过迭代器访问容器中的数据 // vector ::iterator itBegin = v.begin(); // 起始迭代器 指向容器中第一个元素 // vector ::iterator itEnd = v.end(); // 结束迭代器 指向容器中最后一个元素的下一个位置 // // 第一种遍历方式 // while (itBegin != itEnd) // { // cout << \*itBegin << endl; // itBegin++; // } // // 第二种遍历方式 // for (vector ::iterator it = v.begin(); it != v.end(); it++) // { // cout << \*it << endl; // } // 第三种遍历方式 利用STL提供的遍历算法 for_each(v.begin(), v.end(), printFunc); } int main(int argc, char const \*argv[]) { test(); return 0; } 2.5.2 vector存放自定义的数据类型
#include
#include // #include #include using namespace std; // vector容器存放自定义数据类型 class Person { public: Person(string name, int age) { this->m_Name = name; this->m_Age = age; } string m_Name; int m_Age; }; void test01() { // 创建一个vector容器 vector v; Person p1("aaa", 10); Person p2("bbb", 20); Person p3("ccc", 30); Person p4("ddd", 40); Person p5("eee", 50); // 存放自定义数据类型 v.push\_back(p1); v.push\_back(p2); v.push\_back(p3); v.push\_back(p4); v.push\_back(p5); // 遍历容器中的数据 for (vector ::iterator it = v.begin(); it != v.end(); it++) { // cout << "name : " << (\*it).m\_Name << "; age : " << (\*it).m\_Age << endl; cout << "name : " << it->m\_Name << "; age : " << it->m\_Age << endl; } } // 存放自定义的数据类型 指针 void test02() { // 创建一个vector容器 vector v; Person p1("aaa", 10); Person p2("bbb", 20); Person p3("ccc", 30); Person p4("ddd", 40); Person p5("eee", 50); // 存放自定义数据类型 // push_back尾插法 v.push_back(&p1); v.push_back(&p2); v.push_back(&p3); v.push_back(&p4); v.push_back(&p5); // 遍历容器中的数据 for (vector ::iterator it = v.begin(); it != v.end(); it++) { // vector中存放的是地址, cout << "name : " << (*it)->m_Name << "; age : " << (*it)->m_Age << endl; } } int main(int argc, char const *argv[]) { // test01(); test02(); return 0; } 2.5.3 vector容器嵌套容器
vector容器类似一个数组,数组中嵌套数据==》二维数组。
#include
#include using namespace std; void test() { // 创建大容器 vector > v; // 创建小容器 vector v1; vector v2; vector v3; vector v4; // 向小容器中存放数据 for (int i = 0; i < 4; i++) { v1.push_back(i + 1); v2.push_back(i + 5); v3.push_back(i + 9); v4.push_back(i + 13); } // 将小容器存入大容器 v.push_back(v1); v.push_back(v2); v.push_back(v3); v.push_back(v4); // 通过大容器把所有数据遍历一遍 for (vector >::iterator it = v.begin(); it != v.end(); it++) { // *it是小容器vector for (vector ::iterator vit = (*it).begin(); vit != (*it).end(); vit++) { cout << *vit << " "; } cout << endl; } } int main(int argc, char const *argv[]) { test(); return 0; } 3. STL-常用容器
3.1 string容器
3.1.1 string基本概念
本质:string是C++风格的字符串,string本质是一个类
string和char *区别:
·char *是一个指针(C语言风格的字符串本质)
·string是一个类,类内部封装了char *,管理这个字符串,是一个char*型的容器。
特点:
string类内部封装了很多成员方法
例如:查找find,拷贝copy,删除delete,替换replace,插入insert
string管理char *所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责。
3.1.2 string构造函数
1. string(); // 创建一个空字符串
2. string(const char *s) // 使用字符串s初始化
3. string(const string &str) // 使用一个string对象初始化另一个string对象
4. string(int n, char c) // 使用n个字符c初始化
#include
using namespace std; // string的构造函数 /* 1. string(); // 创建一个空字符串 2. string(const char \*s) // 使用字符串s初始化 3. string(const string &str) // 使用一个string对象初始化另一个string对象 4. string(int n, char c) // 使用n个字符c初始化 */ void test01() { // 1. string s1; // string的无参构造 // 构造函数的三种调用方式 string s2("hello world2"); // 括号法 string s3 = string("hello world3"); // 显示法 string s4 = "hello world4"; // 隐式转换法 const char \*str = "hello world"; // 2. string s5(str); cout << "s5: " << s5 << endl; // 3. string s6(s5); // 拷贝构造 cout << "s6: " << s6 << endl; // 4. string s7(10, 'a'); cout << "s7: " << s7 << endl; } int main(int argc, char const *argv[]) { test01(); return 0; } 3.1.3 string赋值操作
string& operator = (const char *s); // char *类型字符串赋值给当前字符串 string& operator = (const string &s); // 把字符串s赋给当前字符串 string& operator = (char c); // 字符赋值给当前字符串 string& assign(const char *s); // 把字符串s赋给当前的字符串 string& assign(const char *s, int n); // 把字符串s的前n个字符赋给当前的字符串 string& assign(const string &s); // 把字符串s赋给当前的字符串 string& assign(int n, char c); // 用n个字符c赋给当前字符串 #include
using namespace std; /* string &operator=(const char *s); // char *类型字符串赋值给当前字符串 string &operator=(const string &s); // 把字符串s赋给当前字符串 string &operator=(char c); // 字符赋值给当前字符串 string &assign(const char *s); // 把字符串s赋给当前的字符串 string &assign(const char *s, int n); // 把字符串s的前n个字符赋给当前的字符串 string &assign(const string &s); // 把字符串s赋给当前的字符串 string &assign(int n, char c); // 用n个字符c赋给当前字符串 */ void test01() { string str1; str1 = "hello world"; cout << str1 << endl; string str2; str2 = str1; cout << str2 << endl; string str3; str3 = 'a'; cout << str3 << endl; string str4; str4.assign("hello C++"); cout << str4 << endl; string str5; str5.assign("hello C++", 4); cout << str5 << endl; string str6; str6.assign(5, 'c'); cout << str6 << endl; } int main(int argc, char const *argv[]) { test01(); return 0; } 3.1.4 string字符串拼接
功能:实现在字符串末尾拼接字符串
函数原型:
string &operator **+=** (const char *str) // 重载+=操作符 string& operator += (const char c); // 重载+=操作符 string& operator += (const string &str); // 重载+=操作符 string& **append**(const char *s); // 把字符串s连接到当前字符串结尾 string& append (const char *s, int n); // 把字符串s的前n个字符连接到当前的字符串结尾 string& append (const string &s); // 同string& operator += (const string &str); string& append (const string &s, int pos, int n);// 字符串s中从pos开始的n个字符连接到字符串结尾
3.1.5 string查找和替换
3.1.6 string字符串比较
3.1.7 string字符串存取
3.1.8 string插入和删除
3.1.9 string子串
3.2 vector容器
3.3 deque容器
3.4 案例
3.5 stack容器
3.6 queue容器
3.7 list容器
3.8 set/multiset容器
3.9 map/multimap容器
4. STL-函数对象
5. STL-常用算法
6. gdb
g++ -g test.cpp -o test
编译时,需要添加-g选项,生成带有包含调试信息(包含源代码的行号,变量名等)的可执行文件
hive
7. 流类库与输入输出
C++/C都没有输入/输出语句,C++标准库中有输入输出的软件包(iostream),这就是I/O流类库。
将数据从一个对象到另一个对象的流动叫做流。
8. 单例模式
记忆知识点
atoi()将字符串转成整型数的函数
main(int argc, char **argv):其中argc是命令行参数,argv是包含所有命令行参数的一个数组
fprintf(stderr, “Usage: %s N\n”, argv[0]);:将一个错误消息打印到标准错误输出(stderr).
argv[0]程序的名字,argv[1]命令行中的第一个参数。
rand()生成0到RAND_MAX之间的伪随机数 浮点数
内存对齐
作用:提高内存访问速度的策略
适用于:不仅有结构体,类,还有各种数组等
内存对齐遵循规则:
1. 对于结构体的各个成员,除了第一个成员的偏移量为 0 外,其余成员的偏移量是 其实际长度 的整数倍,如果不是,则在前一个成员后面补充字节。
2. 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍。
3. 如程序中有 #pragma pack(n) 预编译指令,则所有成员对齐以 n字节 为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。
struct Test01 { char c; short s; int i; double d; }t1; struct Test02 { char c; double d; int i; short s; }t2;
第一个成员 c 的偏移量为 0,所以成员 c 的内存空间的首地址为 0
第二个成员 d 的内存空间的首地址为 8 号地址,偏移量为 8 - 0 = 8(double 类型的整倍数)
第三个成员 i 的内存空间的首地址为 16 号地址,偏移量为 16 - 0 = 16(int 类型的整倍数)
第三个成员 s 的内存空间的首地址为 20 号地址,偏移量为 20 - 0 = 20(short 类型的整倍数)
Test02 所占内存大小为 24 个字节(结构体占用内存大小是结构体内最大数据成员 double 的最小整数倍:24 / 8 = 4)
#include
using namespace std; struct X { char a; // 1 double b; // 8 int c; // 4 } SRT; struct zk02 { /* data */ char a; // 1 int b; // 4 double c; // 8 } SRT2; struct Example2 { double a; int b; char c; } SRT3; struct Example3 { char a; char b; int c; } SRT4; struct Example4 { double a; double b; char c; } SRT5; struct Example5 { char a; char b; char c; double d; } SRT6; int main() { cout << sizeof(SRT) << endl; // 内存对齐 24 cout << sizeof(SRT.a) << endl; // 1 cout << sizeof(SRT.b) << endl; // 8 cout << sizeof(SRT.c) << endl; // 4 cout << sizeof(SRT2) << endl; // 16 cout << sizeof(SRT2.a) << endl; // 1 cout << sizeof(SRT2.b) << endl; // 4 cout << sizeof(SRT2.c) << endl; // 8 cout << sizeof(SRT3) << endl; // 16 cout << sizeof(SRT4) << endl; // 8 cout << sizeof(SRT5) << endl; // 24 cout << sizeof(SRT6) << endl; // 16 return 1; } 箭头(->)和点(.)的区别
箭头(->):左边必须为指针;
点号(.):左边必须为实体
struct MyStruct { int member_a; };
变量MyStruct s; 访问其中元素
s.member_a = 1;
采用指针访问MyStruct *ps; 访问其中元素
(*ps).member_a = 1;
或者使用
ps->member_a = 1;
原码,反码,补码
补码:计算机中使用补码表示整数(负数,正数)
十进制 3(正数) -3(负数) 原码 0000 0011 1000 0011 反码 0000 0011 1111 1100 补码 0000 0011 1111 1101 补码首位的权是负数
八位二进制数,各位的权是:
128 64 32 16 8 4 2 1
例如,一个补码:1110 0000
他代表的十进制是 -128 + 64 + 32 = 32
补码:0110 0000
十进制:0 + 64 + 32 = 96
任意负数的补码(-X),都是0 - X
static const volatile关键字
static、volatile 和 const 关键字的含义和用途:
1. static 关键字:
static被用来控制变量和函数的存储方式(生存期)和可见性(作用域);
1.1 静态全局变量,普通全局变量
全局变量本身就是静态存储方式,(可以被其他文件访问)
加上static,静态全局变量,改变**作用域为定义它的文件,**隐藏它,不被其他文件访问。
1.2 静态局部变量,普通局部变量
加static将局部变量,将其改为静态存储方式。:
- 修饰局部变量:使局部变量在函数调用之间保持其值,即静态存储期。**静态局部变量在第一次初始化后保持其值,而不像普通局部变量每次函数调用都会重新初始化**。
1.3 static函数和普通函数
static函数在内存中只有一份,普通函数在每个被调用的源文件中维持一份拷贝。
- 修饰全局函数:将全局函**数的可见性限制在定义它的文件中**,**即使其他文件中使用相同名称的全局函数也不会引起冲突。** - 修饰类成员变量和函数:使得它们**属于类本身而不是类的实例**,**这些成员变量或函数可以通过类名访问**而不需要创建类的实例。
2. volatile 关键字:
因为访问寄存器比访问内存单元快得多,因此编译器一般会减少内存存取(内存存取优化),但有可能读取脏数据。
-
volatile 用于告诉编译器,被修饰的变量在编译器优化时(不被优化) 不可被省略、重排或缓存。通常用于以下情况:
-
用于描述硬件寄存器或内存映射的变量,因为这些变量的值可能在程序之外被修改。
-
用于多线程环境中,用于描述被多个线程访问的变量,以避免编译器优化引起的问题。
-
在信号处理中,用于保证信号处理函数中对变量的访问不会被优化掉。
3. const 关键字:
简单来说就是“readonly”
-
const 用于创建不可变的常量,可以修饰变量、指针、引用以及类成员函数。
-
修饰变量:创建不可变的变量,一旦初始化后,其值不能再次修改。
-
修饰指针:使指针指向的数据不可修改。
-
修饰引用:创建只读引用,不允许通过引用修改所引用的变量。
-
修饰类成员函数:表示该成员函数不会修改对象的状态(不会修改成员变量)。
动态存储/静态存储方式(生存期)
静态存储方式:程序在编译期分配固定的存储空间。在变量定义时就分配存储单元,直到程序结束(生存期)。全局变量,静态变量都是这种。
动态存储方式:程序运行期间分配存储空间。使用时才分配存储空间,使用结束立即释放。例子:函数的形参,函数定义时并不会分配存储单元,只是在函数被调用时,才分配。函数调用完毕,立即释放。如果一个函数被多次调用,则反复分配、释放形参变量的存储单元。
重载和重写的区别
重载,不考虑返回值类型,只考虑参数列表(参数的类型,个数,顺序)。根据参数列表调用不同的函数。
示例:
重写(派生类中重新定义的函数),派生类中函数的返回值/参数列表都和基类的相同,只有函数的实现不同(花括号中的内容不同)。在派生类被调用时会调用派生类中的函数,不会调用基类中的被重写的函数(基类中的函数需要加上virtual才可以,即基类中的函数是虚函数)。
构造析构函数的调用顺序
其中注意点:
拷贝构造的调用
组合类中 参数的传递是按照引用传递的还是值传递的。值传递需要调用拷贝构造函数,引用传递不需要。
#include
#include using namespace std; class Point { public: Point(int xx = 0, int yy = 0) { x = xx; y = yy; } Point(Point &p); int getX() { return x; } int getY() { return y; } private: int x, y; }; Point::Point(Point &p) { x = p.x; y = p.y; cout << "Calling the copy constructor of Point" << endl; } class Line { public: Line(Point xp1, Point xp2); // 组合类的构造函数 Line(Line &l); // 组合类的拷贝构造函数 double getLen() { return len; } private: Point p1, p2; double len; }; // 组合类的构造函数 Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) // 如果是按引用传递参数的话是这样的 Line::Line(const Point &xp1, const Point &xp2) // 这里加const的作用是防止xp1,xp2对象在Line函数中被修改。 // 按照引用传递会避免不必要对象复制。 { // this->p1 = xp1; // this->p2 = xp2; cout << "Calling constructor of Line" << endl; double x = static_cast (p1.getX() - p2.getX()); double y = static_cast (p1.getY() - p2.getY()); len = sqrt(x * x + y * y); } // 组合类的拷贝构造函数 Line::Line(Line &l) : p1(l.p1), p2(l.p2) { cout << "Calling the copy constructor of Line" << endl; len = l.len; } int main() { Point myp1(1, 1), myp2(4, 5); // 创建myp1,myp2对象的时候没有输出结果。 /* 传入对象的值:调用拷贝构造。拷贝对象副本。 传入对象的引用:调用对象本身 1. 因为这里myp1, myp2是按值传递的,所以会“创建myp1,myp2对象的副本”(这个副本是通过调用Point的拷贝构造函数创建的)。 // 调用2次 2. 在构造函数内部,使用xp1,xp2初始化成员变量p1,p2。因为p1和p2属于Point类型,初始化过程又分别调用Point类的拷贝构造函数。“分别用于从xp1创建p1的副本,从xp2创建p2的副本”。// 调用2次 Point myp1(1, 1), myp2(4, 5);创建了myp1,myp2两个Point对象 Line line(myp1, myp2); 其中myp1,myp2通过值传递,拷贝了myp1,myp2的两个副本,在Line类的构造函数中。又赋初始值从xp1创建p1的副本,从xp2创建p2的副本,创建了两个副本。 */ Line line(myp1, myp2); Line line2(line); cout << "The length of the line is: "; cout << line.getLen() << endl; cout << "The length of the line2 is: "; cout << line2.getLen() << endl; return 0; } 运行结果:
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling constructor of Line
Calling the copy constructor of Point
Calling the copy constructor of Point
Calling the copy constructor of Line
Thelength of the line is:5
The length of the line2 is:5
-
-
-
-
-
- C++ 提供了异常处理机制,允许程序在发生错误时进行有效的资源清理。这在内存和资源管理中尤为重要,因为在没有异常处理的情况下,发生错误可能会导致资源泄漏。
- 封装是面向对象编程的核心概念之一,允许将数据(属性)和用于操作这些数据的代码(方法)绑定到一起作为一个单元(对象),并控制对这些数据的访问。
-
- 这是现代 C++ 中的新特性,允许程序“移动”资源,而非传统的拷贝,可以提高资源管理的效率,特别是对于那些分配了大量内存或占用了其他重要资源的对象。
- 用于控制对象的拷贝行为,特别是涉及到动态内存分配的对象。用户可以定义自己的拷贝构造函数和拷贝赋值操作符,以实现深拷贝或禁止对象拷贝等行为。
-
-
-
-
-
-
-
-
-
-
-
猜你喜欢
- 3小时前[AxiosError]: There is no suitable adapter to dispatch the request since :- adapter xhr is not suppo
- 3小时前判断自己的mac是macOS x64 、 macOS ARM64
- 3小时前基于微信上海某大学浴室预约小程序系统设计与实现 研究背景和意义、国内外现状
- 3小时前ffmpeg实现视频解码
- 3小时前第6章-路由器、交换机及其操作系统介绍
- 3小时前Android13音频子系统分析(三)---音效算法集成框架
- 3小时前【赠书第17期】Excel高效办公:文秘与行政办公(AI版)
- 3小时前游戏开发中的噪声算法
- 3小时前Hutool工具包中HttpUtil的日志统一打印以及统一超时时间配置
- 3小时前AWS CICD之二:配置CodeDeploy
网友评论
- 搜索
- 最新文章
- 热门文章