黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难_哔哩哔哩_bilibili
12.4 : 学到了多态就没有往下面学了, 慢慢学吧
#include
namespace namespaceName //我们使用了 namespace 定义了一个命名空间 namespaceName。
using namespace...; //命名空间引入
using namespace std; // std命名空间引入.我们常用的输入和输出 函数 都是定义在 std 命名空间中的,因此,我们需要使用输入和输出,必须要引入 std 命名空间
using std::cout; //使用 using 限定符,引入 std 命名空间指定内容cout
#include //c++中没有自带string数据类型,因此如果要用string数据类型,要用头文件. 后续就可以string name,否则用string会报错
//普及章节
// 0.1
// C++ 其实是在 C 语言的基础上增加了新特性,因此,取名为 C++。 用 C++ 编写的程序可读性好,生成的代码质量高,运行效率仅比汇编语言慢 10%~20%。
// 区分 C 语言,如内联函数、函数重载、名字空间、更灵活、方便的内存管理(new、delete)、引用。
// C++应用领域:服务 器端开发, 游戏, 虚拟现实, 数字图像处理, 科学计算, 网络软件, 操作系统,嵌入式系统等
// 0.2 C++语言编译链接教程
// C++语言开发环境:Windows 本身就自带 C++ 语言的运行环境,因此,为了开发 C++ 语言,我们只需要安装一个 C++ 语言的开发工具即可。
// 我们编写好 C++ 语言 的程序后,需要运行 C++ 程序,必须要经过编译与链接的过程。=在编辑器上点击 编译并运行 按钮
// 将 C++ 代码转换成 CPU 能够识别的二进制指令,也就是将代码加工成 .exe 程序的格式。这个工具是一个特殊的软件,叫做编译器(Compiler)。编译也可以理解为 “翻译”
// C++ 的编译器有很多种: Windows 下是Visual C++,它被集成在 Visual Studio 中. Linux 自带 G++.
// Mac 下常用的是 LLVM/Clang,它被集成在 Xcode 中.
// .c 源代码文件
// .C .cc 或 .cxx 为后缀的文件,源代码文件另有 .cpp、 和 .c++。
// .a 由目标文件构成的档案库文件
// .h 程序所包含的头文件. .h 里面只有声明,没有实现. 有using namespace std.
// .hpp 程序所包含的头文件. .hpp 里声明实现都有,可以减少 .cpp 的数量. 无using namespace std.
// .i 已经预处理过的 C 源代码文件
// .ii 已经预处理过的 C++ 源代码文件
// .m Objective-C 源代码文件
// .o 编译后的目标文件 对于 Visual C++,目标文件的后缀是 .obj,对于 G++,目标文件的后缀是 .o
// .s 汇编语言源代码文件
// C++五大标准:98,11,14,17,20. 不同标准表示不同年份
//静态库和静态库
// 在 Linux 中,库文件分成静态库和共享库两种。静态库以.a 作为后缀名,共享库以.so 结尾。所有库都是一些函数打包后的集合,
// 差别在于静态库每次被调用都会生成一个副本,而共享库则只有一个副本,更省空间。(通过l -lh查看)
// 静态库用的时候方便,动态库省内存.
// 静态库最后都会被集成到可执行文件中,动态库 不会
// 类比windows下的 dll 与lib
// g++ main.cpp -Iinclude -lswap -Lsrc -o dyna_or_static, 这条命令到低调用的是动态库?还是静态库呢?
// 应该是同时有动态和静态库存在时优先链接动态库
// 这里的例子做的不太好,动态静态库做好以后,应该单独放出来编译,在原位置编译可能编译的是源码
// 静态库,程序编译后,直接链接,生成最终文件,如果改了一部分主文件源码,需要重新链接。
// 动态库,程序运行在内存后,再链接库文件,如果修改了源码,重新编译链接部分不涉及动态库文件,所以更加方便。
// 相对而言,静态库链接的文件也更大一些,毕竟是运行前就链接了,相当于提前拷贝了源码到主程序。
// vs才是ide,vscode其实算不上IDE
// vscode就是个编辑器,加了各种插件才是IDE
/* 终端命令
g++ helloworld.cpp 打开helloworld
g++ helloworld.cpp -o h o新建一个可编辑的cpp文件,命名为h
./helloworld 执行helloworld
*/
//第一章 C++ 变量类型:
// 变量声明.
extern int a, b;
extern int c;
extern char f; //得变量可以跨文件被访问
int j; // 全局变量声明.全局变量一旦声明,在整个程序中都是可用的。定义全局变量时,系统会自动初始化为0 (区分局部变量)
// 常量声明.在 C++ 中,有两种简单的定义常量的方式:#define,或const. 请注意,把常量定义为大写字母形式
85 // 十进制.不带前缀则默认表示十进制。
0213 // 八进制 .0 表示八进制
0x4b // 十六进制 .0x 或 0X 表示十六进制
30 // 整数
30u // 无符号整数 .U 表示无符号整数
30l // 长整数 .L 表示长整数
30ul // 无符号长整数
314159e-5 = 314159 * 10 ^ (-5) // 指数
314159E-5L // 合法的
510E // 非法的:不完整的指数
210f // 非法的:没有小数或指数
21.0f // 合法的
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
int i = -1000;
double d = 200.374;
float f = 230.47;
cout << "abs(i) :" << abs(i) << endl; // 1000 绝对值
cout << "floor(d) :" << floor(d) << endl; // 200. 向下取整
cout << "sqrt(f) :" << sqrt(f) << endl; // 15.1812 . 该函数返回平方根
cout << "pow( d, 2) :" << pow(d, 2) << endl; // 40149.7 . 该函数返回 d的 2 次方。
// C++ 随机数: rand(),该函数只返回一个伪随机数。生成随机数之前必须先调用 srand() 函数。
int main()
{
int i, j;
// 实例中使用了 time() 函数来获取系统时间的秒数,作为参数传入srand
srand((unsigned)time(NULL)); //调用 srand() 函数
/* 生成 10 个随机数 */
for (i = 0; i < 10; i++)
{
j = rand(); // 调用 rand() 函数,生成实际的随机数
cout << "随机数: " << j << endl;
}
}
// 随机数: 1748144778
// 随机数: 630873888
// 随机数: 2134540646
//--------------------------------------------------------
int func(); // 函数声明
int main() // main() 是程序开始执行的地方
/*
这是多行注释 */
{ // 语句块是一组使用大括号括起来的按逻辑连接的语句
cout << "Hello, world!" << endl; //你可以用 "\n" 代替 endl
return 0; //为了增强可读性,您可以根据需要适当增加一些空格
// enum color { red, green, blue } c;
// c = blue;
// cout << c
int a = 3, b = 5; // 定义并初始化 a 和 b
int c; // 定义 c
c = a + b; // 初始化 c
char f = 'f'; // 变量 f 的值为 'f'
cout << c << endl; // 8
f = '70.0/3.0'; // 70.0/3.0'
cout << f << endl;
int j; // 在函数或一个代码块内部声明的变量,称为局部变量。它们只能被函数内部或者代码块内部的语句使用。在函数内,局部变量的值会覆盖相同名称的全局变量的值
// 当局部变量被定义时,系统不会对其初始化.
int i = func(); // 函数调用
cout << "Hello\tWorld, \
runoob\n\n"; // Hello World,runoob. \t空格,\把一个很长的字符串常量进行分行,\n换行
short int i; // 有符号短整数
short unsigned int j; // 无符号短整数
j = 50000;
i = j;
cout << i << " " << j;
return 0;
// 函数定义.在函数声明时,提供一个函数名,而函数的实际定义则可以在任何地方进行。
int func()
{
return 0;
}
}
//--------------------------------------------------------
// C++ 程序中可用的存储类/关键字:static, extern, mutable, thread_local (C++11)
//一,static和extern:见https://www.jianshu.com/p/a7a346408d4a
//内部变量=局部变量. 外部变量=全局变量.
// static能够声明和定义一个内部函数;static能够定义一个内部变量,并将其延长至程序结束. extern能定义和声明一个外部函数,extern只起到声明一个外部变量的作用.
//大工程下我们会碰到很多源文档。下面假设static出现在a文档:
static int i; // i变量只在a文档中用(static能够定义一个内部变量)
int j; // j变量,在工程里用
static void init() // init方法只在a文档中用 (static能够声明和定义一个内部函数)
{
}
void callme() // callme方法,在工程里用
{
static int sum; //变量sum的作用域只在callme里 (static能够定义一个内部变量)
}
// extern告诉编译器这个变量或函数在其他文档里已被定义了.下面假设在文档b中:
extern int j; //调用a文档里的( extern只起到声明一个外部变量的作用.声明后要另外定义,如下面的main函数)
extern void callme(); //调用a文档里的外部函数( extern能定义和声明一个外部函数(extern可以省略,本来函数就默认外部))
int main()
{
j = 10;
return 0;
}
//------------------对于内部变量:static延长局部变量的生命周期:程序结束的时候,局部变量才会被销毁
void test()
{
static int b = 0; // static延长局部变量的生命周期,程序结束的时候,局部变量才会被销毁.所有的test函数都共享着一个变量b.
b++;
printf("b的值是%d\n", b); // 3 //如果没有static,代码块test每次被调用后变量b就自动销毁,下次调用的时候又重新分配内存空间.此处b的值是0
}
int main()
{
test(); // b的值是1 //如果没有static,此处b的值是0
test(); // b的值是2 //如果没有static,此处b的值是0
test(); // b的值是3 //如果没有static,此处b的值是0
return 0;
//第二章 C++运算符. &&与,||或. //&与,|或,^异或,~取反
// &与,都为1才是1,否则为0. |或,都为0才是0,否则为1. ^异或,都为0或都为1,即为0.不同则为1. ~取反.针对二进制取反,因此要先转换成二级制, ~1=-2;
int d = 10; // 测试自增、自减
c = d++; // d++时语句中d的值为10,语句执行完后d的值为11.
//区分++d,++d时语句中d的值为11,语句执行完后d的值仍为11.
cout << "Line 6 - c 的值是 " << c << endl;
d = 10; // 重新赋值
c = d--;
cout << "Line 7 - c 的值是 " << c << endl; // d的值为9
// 运算符是二进制之间的比较,要先将十进制改为二进制
unsigned int a = 60; // 60 = 0011 1100
unsigned int b = 13; // 13 = 0000 1101
int c = 0;
c = a & b; // c= 12 = 0000 1100
c = a | b; // 61 = 0011 1101
c = a ^ b; // 49 = 0011 0001
c = ~a; // -61 = 1100 0011
c = a << 2; // 240 = 1111 0000
c = a >> 2; // 15 = 0000 1111
// &(按位与)和&&(逻辑与): 相同:当与号两边为0的时候,结果均为假=0
// 区别: &&是短路运算符,即如果&左边判断为假,&右边就不会执行了,直接判断为假.
// &: 如果&左边判断为假,&右边仍然会执行
// eg: a=5 if(0&(a--)){...} //判断为假,但a变成了4
// if(0&&(a--)){...} //判断为假,但a还是5
a = 0; // a=0000
b = 10; // b=1010
//(a,b虽然是4位,但是int都是32位,这里是把前面18个0都省略了.为什么可以省略?可以看到,哪怕省略了0,值仍然是等价的. 1010=00000000...1010)
if (a && b) // a && b结果为false. 0000和1010相与的结果为0000, 此处的if ( a && b ) 等价为if (false )
{
cout << "Line 3 - 条件为真" << endl;
}
else //此处等价于 if (true )
{
cout << "Line 4 - 条件不为真" << endl;
}
if (!(a && b)) // !(a && b)>> !(false)>>true , 此处的if ( !(a && b) ) 等价为if (true )
{
cout << "Line 5 - 条件为真" << endl; // Line 4 - 条件不为真 Line 5 - 条件为真
unsigned int a = 60; // 60 = 0011 1100
unsigned int b = 13; // 13 = 0000 1101
int c = 0;
c = ~a; // -61 = 1100 0011 ~和反码的区别: 取反包括符号位. 这里的1100 0011其实是1000....0100 0011的
c = a << 2; // 240 = 1111 0000 二进制左移2位.符号位不变,数值位左边丢弃(不管是0还是1都丢弃),右边补0
c = a >> 2; // 15 = 0000 1111 二进制右移2位.符号位不变,数值位右边丢弃(不管是0还是1都丢弃),左边补0(负数左补1)
//右移1位>>除以2, 右移2位>>除以4
//取反的结果是补码
//第三章 循环
// while 循环 while(条件){循环体}
int a = 10; //局部变量声明
while (a < 20)
{
cout << "a 的值:" << a << endl;
a++;
}
// for 循环执行 for(起点;条件;步骤){循环体}, 等效于while循环. 构成循环的三个表达式中任何一个都不是必需的。for( ; ; )无限循环. Ctrl + C 键终止一个无限循环
for (int a = 10; a < 20; a = a + 1)
{
cout << "a 的值:" << a << endl;
}
// for 循环小范围迭代 for(变量:迭代范围){循环体}
int my_array[5] = {1, 2, 3, 4, 5};
for (auto &x : my_array)
{ // 冒号前为迭代的变量,冒号后为被迭代的范围. auto 类型也是 C++11 新标准中的,用来自动获取变量的类型
x *= 2;
cout << x << endl;
// do...while 循环 . do{循环体}while(条件); 区分while: 条件出现在循环的尾部,所以循环体至少会执行一次。
int a = 10;
do
{
cout << "a 的值:" << a << endl;
a = a + 1;
if (a > 15)
{
break; // break跳出while循环. 区分continue:跳出本次while循环,进入下一轮判断.
}
} while (a < 20);
//第四章 判断.
// if(bool判断){条件为真的执行语句}
int a = 10;
if (a == 10)
{
cout << "a 的值是 10" << endl;
}
else if (a == 20)
{
cout << "a 的值是 20" << endl;
}
else if (a == 30)
{
cout << "a 的值是 30" << endl;
}
else
{
cout << "没有匹配的值" << endl;
}
cout << "a 的准确值是 " << a << endl;
//没有匹配的值 a 的准确值是 100
// switch-case判断.是对定值的判断。如评级是A/B/C,工资明年到底是+100还是+200还是+300。
// switch(grade){case'A':} 相当于 if(grade=A){}. if else-if 处理的是带范围的判断(if grade>A)。
int salary = 1000;
switch (grade)
{
case 'A':
salary += 100;
break;
case 'B':
salary += 200;
break;
case 'C': 如果冒号后面的语句和下面一样,可以省略。一直到不一样为止
break;
case 'D':
salary += 300;
break;
default: //跟以上的case都不符合,则执行default. switch中的default相当于if中的else
cout << "无效的成绩" << endl;
}
cout << "您的工资是 " << salary << endl;
/*三元表达式:能用if-else表示的,都可以考虑用三元表达式
表达式1?表达式2:表达式3;
表达式1一般为一个关系表达式。
如果表达式1的值为true,那么表达式2的值就是整个三元表达式的值。
如果表达式1的值为false,那么表达式3的值就是整个三元表达式的值。
注意:表达式2的结果类型必须跟表达式3的结果类型一致,并且也要跟整个三元表达式的结果类型一致。*/
int x, y = 10;
x = (y < 10) ? 30 : 40;
cout << "value of x: " << x << endl; // value of x: 40
//第五章 函数. 又叫方法、子例程或程序
//每个 C++ 程序都至少有一个函数,即主函数 main() .函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。
//内置函数。例如,函数 strcat() 用来连接两个字符串,函数 memcpy() 用来复制内存到另一个位置。
// 1.函数的声明: 在函数定义前告诉编辑器有这个函数的存在.有了声明就可以先调用再定义函数,即定义写在调用后面
// 函数声明可以有多次,但是定义只能有一次
// 格式: 函数名(形参);
//在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:int max(int, int);
// 2.函数的定义
// 返回类型 函数名称(形参)
// {
// 函数体
// renturn 返回值
// }
// 如果函数不需要返回值,声明的时候可以写void,然后函数体中直接写"return;"即可,或根本不写return也行.
// 如果没有指定返回类型,则默认为int
// 3.函数的调用: 函数名(实参)
int max(int num1, int num2); // 函数声明
int main()
{
int a = 100; // 局部变量声明
int b = 200;
int ret;
ret = max(a, b); // 调用max函数来获取最大值,传入实参. ret来接受max函数的返还结果
cout << "Max value is : " << ret << endl;
return 0;
}
int max(int num1, int num2) //定义函数 //形参. 值传递:函数调用时,实参将数值传递给形参
{
int result; // 局部变量声明
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
// 4.函数传递参数的方式有三种:
//传值调用:把参数的'实际值'赋值给形参。形参不改变实参.
//指针调用:把参数的'地址'赋值给形参。形参改变实参.
//引用调用:把参数的'引用的地址'赋值给形参。形参改变实参.
//传值调用:
void swap(int x, int y);
int main()
{
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
swap(a, b); // 调用函数来交换值,换的是值,不是地址
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
// 交换前,a 的值: 100
// 交换前,b 的值: 200
// 交换后,a 的值: 100
// 交换后,b 的值: 200
//指针调用:
void swap(int *x, int *y);
int main()
{
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
swap(&a, &b); //&a 表示指向 a 的指针,即变量 a 的地址 . &b 表示指向 b 的指针,即变量 b 的地址
// &a是a的内存地址的二进制表示
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
// 交换前,a 的值: 100
// 交换前,b 的值: 200
// 交换后,a 的值: 200
// 交换后,b 的值: 100
//引用调用:
void swap(int &x, int &y); //在 swap()函数的声明和定义 中,您需要声明函数参数为引用类型
int main()
{
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
swap(a, b);
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
// 交换前,a 的值: 100
// 交换前,b 的值: 200
// 交换后,a 的值: 200
// 交换后,b 的值: 100
// 5.匿名函数 Lambda 函数(也叫 Lambda 表达式)
// 6.函数高级-- 函数默认参数
//在C++中,函数的形参列表中的形参是可以有默认值的
//语法: 返回值类型 函数名 (参数= 默认值){}
//如果我们自己传入了数据,就用自己的数据,如果没有,就用默认值
// 函数声明和实现只能有一个可以有默认参数
// 6.1 正常方式,参数无默认值的情况
int func1(int a, int b, int c)
{
return a + b + c;
}
// 6.2 参数有默认值的情况:
//如果某个位置已经有了默认参数,那么从这个位置以后,从左到右都必须有默认值 .如果b有默认参数,则c也必须有默认参数
int func2(int a, int b = 20, int c = 30) //如果b有默认参数,则c也必须有默认参数
{
return a + b + c;
}
//如果函数的声明有默认参数,函数实现就不能有默认参数. 声明和实现只能有一个有默认参数
int func3(int a = 10, int b = 10); //函数的声明
int func3(int a, int b) //函数的实现(函数体)
// int func2(int a = 10, int b = 10) //报错. 声明了有默认参数,函数实现就不能有默认参数
{
return a + b;
}
int main()
{
cout << "func1 = " << func3(20, 30, 30) << endl; // 80
cout << "func2 = " << func1(10) << endl; // 60 只传了1个参数a
cout << "func2 = " << func1(10, 30) << endl; // 70 传了参数a和b
cout << "func3 = " << func2(20) << endl; // 30 优先使用自己传入的数据
cout << "func3 = " << func2() << endl; // 20
}
// 7. 函数高级-- 函数占位参数
// C++中函数的形参列表里可以有占位参数,用来做占位. 调用函数时必须填补该位置
// 语法: 返回值类型 函数名 (参数数据类型){}
void func(int a, int) //第二个int就是占用参数.
// void func(int a, int) //占位参数还可以有默认参数
{
cout << "this is func " << endl;
}
int main()
{
func(10, 20); //占位参数必须填补, 20必须传入
}
// 8.函数高级-- 函数重载
// 函数重载:函数名可以相同,提高复用性
// 函数重载满足三个条件:
// 同一个作用域下:如都在全局作用域下
// 函数名称相同:void func()
// 函数参数不同: 类型不同 或者 个数不同 或者 顺序不同
// 注意: 函数的返回值不可以作为函数重载的条件
void func()
{
cout << "func的调用" << endl;
}
void func(int a)
{
cout << "func(int a)的调用" << endl;
}
void func(int a, double b)
{
cout << "func(int a,double b)的调用" << endl;
}
void func(double a, int b) //参数顺序不同, 也是重载
{
cout << "func(double a, int b)的调用" << endl;
}
// int func(double a, int b) //报错 . 函数的返回值不可以作为函数重载的条件.返回类型由void改为int,不是重载
// {
// cout << "func(double a, int b)的调用" << endl;
// }
int main()
{
func(); //调用的是func()
func(10); //调用的是func(int a)
func(10, 3.14); //调用的是func(int a, double b)
func(3.14, 10); //调用的是func(double a, int b)
}
// 3.4 函数重载注意事项
// 引用作为重载参数, 变量传入int& a ,常量传入const int& a
// 函数重载碰到参数有默认值, 容易报错
// 3.4.1 引用作为重载参数, 变量传入int& a ,常量传入const int& a
// int和const int参数属于 类型不同的参数,可以构成重载.
void func(int &a) //如果传进a, 则int &a = 10;不合法
{
cout << "func(int &a)的调用" << endl;
}
void func(const int &a) // const只读,const int &a = 10;合法
{
cout << "func(const int &a)的调用" << endl;
}
int main()
{
int a = 10;
func(a); //调用的是第一个func. 因为a是可读可写的变量,如果传进第二个func,就只能读,不能写
func(10); //调用的是第二个func.
}
// 3.4.2 函数重载碰到参数有默认值, 会报错
void func2(int a, int b = 10) //参数b有默认值
{
cout << "func2(int a, int b)的调用" << endl;
}
void func2(int a)
{
cout << "func2(int a)的调用" << endl;
}
int main()
{
int a = 10;
func2(10); // 传入两个函数都可以, 有歧义, 会报错
}
//第六章 数组. 数组都是由连续的内存空间组成。
// 1.定义一维数组 :有三种方式
// 数据类型 数组名称[元素个数]; 然后再赋值,如arr[0]=50
// 数据类型 数组名称[元素个数] = {...}; eg.double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
// 数据类型 数组名称[] = {...}; eg.int arr[]={}
// 1.1打印内存地址
// sizeof(arr)<< 求数组占用的内存空间,如40=40字节=10个int(每个int占4个字节)
// arr <<打印数组名称可以直接打印数组的首内存地址,如0x0000
// arr[0] <<打印数组中第一个元素的内存地址,打印效果与上面打印arr相同
//(int)&arr[0] <<打印数组中第一个元素的内存地址,并令其变为int格式
// eg.float a=8.0 int(a)=8
//(int)&a实际是将二进制的地址转化成int型; &a则是'浮点数'a的内存地址的二进制表示;
// (int&)a则是告诉编译器将a当作'整数'看(并没有做任何实质上的转换)。
//以整数形式存放和以浮点形式存放其内存数据是不一样的,因此(int)&a, (int&)a两者不等。
// 2.定义二维数组.多行的一维数组,行*列矩阵. 有四种方式:
// 数据类型 数组名称[行数][列数]; 然后再赋值,如arr[0]=50
// 数据类型 数组名称[行数][列数] = {{...},{...},...}; int arr[2][3]={{1,2,3},{4,5,6}}
// 数据类型 数组名称[行数][列数] = {...}; int arr[2][3]={1,2,3,4,5,6}
// 数据类型 数组名称[][列数] = {...}; //可以省略行数,不能省略列数. {{...}}内嵌则行列数都不能省略
// int arr[2][3]={{1},{2}}
// 1 0 0
// 2 0 0
// int arr[2][3]={1,2,3}
// 1 2 3
// 0 0 0
//第七章 字符串. c++的字符串有两种表示形式
// 1.以 null 结尾的字符串. 本质上是使用 null 字符 \0 终止的一维字符数组. \0可省略,编译器会自动加上
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
cout << site << endl; // RUNOOB
char str1[13] = "runoob";
char str2[13] = "google";
char str3[13];
int len;
strcpy(str3, str1); // 复制 str1 到 str3
cout << "strcpy( str3, str1) : " << str3 << endl; // runoob
strcat(str1, str2); // 拼接 str1 和 str2
cout << "strcat( str1, str2): " << str1 << endl; // runoobgoogle
len = strlen(str1); // 连接后,str1 的总长度
cout << "strlen(str1) : " << len << endl; // 12
// 2. string
//区分char是字符型,2个字节.
// string字符串在C语言中没有这种类型,是python中的基础数据类型.C++中的string表示类,没有固定内存大小,大小由内部含有多少字符决定
string str1 = "runoob";
string str2 = "google";
string str3;
int len;
str3 = str1; // 复制 str1 到 str3
cout << "str3 : " << str3 << endl; // runoob
str3 = str1 + str2; // 连接 str1 和 str2
cout << "str1 + str2 : " << str3 << endl; // runoobgoogle
len = str3.size(); // 连接后,str3 的总长度
cout << "str3.size() : " << len << endl; // 12
//第八章 指针
//什么是内存位置? 每一个变量都有一个内存位置, 用十六进制数表示
//如何访问内存位置? 取址符号. &var1:0xbfebd5c0
//指针,全称指针变量. 指针即地址!
// 1.定义指针:数据类型 *指针变量名. 指针可以是int类型,double,float,char类型
// eg. int *p. *说明是p指针
// 1.1 让指针记录变量a的地址
int a = 10 int *p
p = &a
cout
<< p << endl; // 0x0000
//上面的集合写法
int a = 10 int *p = &a
//强制转换
int *p = (int *)0x1100 //强制转换成int类型的指针
// 1.2 可以通过解引用(*)指针的方式来找到指针指向的内存,并对内存中存放的数据进行读写操作 p>>0x0000>>10
*p = 1000;
cout << a << endl; // 1000
cout << *p << endl; // 1000
// eg.
int var = 20; // 实际变量的声明
int *p; // 指针变量的声明,一般用p表示
ip = &var; // 在指针变量中存储 var 的地址
cout << var << endl; // 20
cout << ip << endl; // 输出在指针变量中存储的地址:0xbfc601ac
cout << *ip << endl; // 访问指针中地址的值:20
// 1.3 指针所占的内存空间:在32位操作系统(操作系统基本都是32位),占4个字节空间. (在64位系统中占8个字节)
cout << sizeof(int *) << endl; // 4
cout << sizeof(*) << endl; // 4
cout << sizeof(p) << endl; // 4
// 2.空指针
//空指针指向内存编号为0的内存空间.
// 2.1 用于初始化指针:新建的指针一开始不知道指向哪里好,就指向0编号的内存.0=NULL
int *p = NULL;
// 2.2 空指针指向的内存是不可以访问的. 0-255之间编号的内存是系统占用的,不可以访问
*p = 100 //空指针不能赋值
// 3.野指针
//野指针指向非法的内存空间. 该内存空间没有经过申请
// 3.1 在程序中,尽量避免出现野指针
int *
p = (int *)0x1100 //在系统没有分配0x1100这个空间的情况下,让指针指向这个空间,会报错:访问权限冲突
//空指针和野指针都不是我们申请的空间,因此不要访问,访问都会出错
system('pause')
// 4.const修饰指针.
//快速记忆:const叫常量,*叫指针
// 4.1 const修饰指针时,为常量指针
// 特点:指针的指向可以修改,但指针指向的值不可以改
int a = 10 int b = 10 int * p = &a const int * p = &a // const叫常量,*叫指针,>>常量指针
// const后面跟着*,那么取*的操作就不能做,如*p=20
* p = 20 //错误.指针指向的值不可以改
p = &b //正确.针的指向可以修改
// 4.2 const修饰常量时,为指针常量
// 特点:指针的指向不可以改,但是指针指向的值可以改
int *const p = &a; // const后面跟着p,那么变p的操作就不能做,如p=&b
*p = 20 //正确.指针指向的值可以修改
p = &b //错误.指针指向不可以改
// 4.3 const修饰指针和常量时,为修饰常量
//特点:指针的指向, 指针指向的值 都不可以改
const int *const p = &a;
*p = 20 //错误.指针指向的值不可以改
p = &b //错误.针的指向也不可以修改
// 5.指针和数组配合: 利用指针访问数组中的元素
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
cout << "第一个元素为:" << arr[0] << endl;
int *p = arr; // arr就是数组首地址
cout << '利用指针访问第一个元素:' << *p << endl; // 1
p++; //指针向后移动4个字节,每个int是4个字节大小
cout << '利用指针访问第二个元素:' << *p << endl; // 2
//遍历数组方法一
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//遍历数组方法二:指针法
int *p2 = arr for (int i = 0; i < 10; i++)
{
cout << *p2 << endl;
p2++;
}
// 6.指针和函数: 指针作为函数形参,可以改变实参的值
//值传递
//值传递时,形参中接收的是a和b的值 .形参改变,不改变实参.见下方main函数
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
//函数内部成功交换: a=20,b=10
}
//地址传递
//地址传递时,实参中传递的是'a的&取址',形参接收的时候又进行了'解引用*p指针' .
//地址中交换的是地址.形参改变,会改变实参.
void swap2(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
//函数内部成功交换: a=20,b=10.
}
int main()
{
int a = 10;
int b = 20;
swap1(a, b); //值传递不会改变实参
swap2(&a, &b); //地址传递会改变实参.
cout << "a=" << a << endl;
cout << "b=" << b << endl;
//值传递函数外部,不交换: a=10,b=20
//地址传递函数外部,实参数据也会交换: a=20,b=10
}
//第九章 结构体
// 1. 结构体:用户自定义的数据类型. 将好多种数据类型(如下面student中有string,int,int三种类型)集中到一起,组装成一个数据类型.
// 定义结构体: struct 结构体名{成员列表} //结构体成员列表=属性
// 结构体变量创建方式1:
// struct 结构体名 变量名;
// 变量名.成员=成员值;
// 结构体变量创建方式2: struct 结构体名 变量名={成员1值,成员2值...}
// 结构体变量创建方式3: 在定义结构体时,顺便创建变量
//如自创"学生"的数据类型,之后可以快速创建"学生"变量,学生包括三个属性:name,age,score
struct student
{
string name; //属性=成员列表
int age;
int score;
// }; //定义student结构体数据类型
} stu3; //结构体变量创建方式3
int main()
{
struct student stu1; //结构体变量创建方式1
stu1.name = "张三";
stu1.age = 18 stu1.score = 100;
cout << "姓名:" << stu1.name << "年龄:" << stu1.age << "分数:" << stu1.score
struct student stu2 = {"李四", 19, 60}; //结构体变量创建方式2
cout << "姓名:" << stu2.name << "年龄:" << stu2.age << "分数:" << stu2.score
}
// 2.结构体数据
//结构体数组, 数组中每个元素都是自定义的结构体
// 结构体数组创建方式1: struct 结构体名 数组名[元素个数] = { {成员1值,成员2值...},{成员1值,成员2值...},{成员1值,成员2值...}...};
// 结构体数组创建方式2:
// struct 结构体名 数组名[元素个数];
// struct 结构体名 数组名[元素索引].成员=成员值;
// 可以在定义数组时给元素赋值,也可以先定义数组之后再给数组中的元素赋值,这也是数组的修改方式
struct Student
{
string name;
int age;
int score;
}; // 定义student结构体数据类型
int main()
{
struct Student stuArray[3] = //结构体数组创建方式2
{
{"张三", 18, 60};
{"李四", 19, 60};
{"王五", 38, 66};
};
//结构体数组创建方式1 .也是数组的修改方式
stuArray[2].name = "赵六";
stu1.age = 18 stu1.score = 100;
//遍历结构体数组
for (int i = 0; i < 3; i++)
{
cout << " 姓名:" << stuArray[i].name
<< " 年龄:" << stuArray[i].age
<< " 分数:" << stuArray[i].score << endl; //这三行是一句话,因此前两行没有分号;
}
system("pause");
return 0;
}
// 3.结构体指针
// 通过指针来访问结构体中的成员。指针->成员
// 利用运算符 -> 来访问结构体的属性,也就是将 . 换成 -> , 就可以通过指针来访问。-和>拼接
struct Student //定义结构体
{
string name;
int age = 0;
int score = 0;
};
int main()
{
struct Student s = {"张三", 18, 91}; //定义结构体变量,并赋值. 这里的struct可以省略.
struct Student *p = &s; //定义指针,并指向结构体变量. 注意:接收的指针类型要和结构体类型保持一致,Student. 这里的struct可以省略.
cout << " 姓名:" << p->name //利用 -> 来访问结构体变量的属性,并输出相应内容
<< " 年龄:" << p->age
<< " 分数:" << p->score << endl;
system("pause");
return 0;
}
// 4.结构体嵌套结构体
// 结构体中的成员可以是另外一个结构体。
// 例:老师一对一辅导学生.老师这个结构体中,记录自己带的是哪个学生:
// 首先定义一个老师的结构体,结构体成员有:老师的姓名、年龄、职工号、辅导的学生,
// 此时这个辅导的学生就可以是另外一个结构体. 学生的结构体成员有:学生的姓名、年龄、成绩等....
struct Student //定义学生的结构体
{
string name; //学生姓名
int age = 0; //学生年龄
int score = 0; //学生成绩
};
struct Teacher //定义老师的结构体
{
string name; //老师姓名
int age = 0; //老师年龄
int id = 0; //老师工号
struct Student s; //辅导的学生. 如果先定义老师的结构体,再定义学生的结构体,写到这句话可能会报错,因为此时没有学生的结构体
//因为系统不认识学生的结构体,所以不管什么版本,最好先定义出现的子结构体。
};
int main()
{
struct Teacher t; //创建结构体变量
t.name = "万华";
t.age = 48;
t.id = 336;
t.s.name = "张三"; //第一个 . 是访问t中的属性,第二个 . 是访问s中的属性
t.s.age = 16; //因为我们老师的结构体中,还嵌套有一个学生的结构体。
t.s.score = 96;
cout << "老师的姓名:" << t.name
<< " 老师的职工号:" << t.id
<< " 老师的年龄:" << t.age << endl
<< "老师辅导的学生姓名:" << t.s.name
<< " 老师辅导的学生年龄:" << t.s.age
<< " 老师辅导的学生成绩:" << t.s.score << endl;
system("pause");
return 0;
}
// 5.结构体作为函数参数
//结构体作为函数的参数向函数中传递.有值传递和地址传递。
//值传递:形参改变 不改实参. 地址传递:形参改变 改变实参.
//为什么说将函数的形参改为指针,可以节省内存空间? 答:
//值传递传递参数s的时候,是将s拷贝了一份放入函数中,
//因此函数内部的s 与函数外部的s不同. 拷贝的s是占了内存空间的,内存空间*2.
//如果传入的是*s,即指针,就不用拷贝粘贴.无论s多大,一个指针的大小是固定的,如int*p就是4个字节,
struct Student
{
string name;
int age = 0; // int age; 也可
int score = 0; // int score; 也可
};
//值传递.
void printStudent1(struct Student s) //定义打印学生信息的方法
{
s.age = 100;
cout << "在print1函数中打印的结果:" << endl
<< "姓名:" << s.name
<< " 年龄:" << s.age // 100
<< " 成绩:" << s.score << endl;
}
//地址传递
void printStudent2(struct Student * p) //指针的数据类型要和传入的s保持一致,Student
{
p->age = 200;
cout << "在print2函数中打印的结果:" << endl
<< "姓名:" << p->name //指针用 -> 访问
<< " 年龄:" << p->age
<< " 成绩:" << p->score << endl;
}
//地址传递 +const防止误修改
//为什么值传递中不加const修饰? 因为值传递中的s变化本身就不会影响函数外的s
void printStudent2(const Student *p) //数据类型Student前面加入const
{
p->age = 200; //会报错.因为加了const修饰后,就不可以修改了.只可以访问.根据需要加.
cout << "在print2函数中打印的结果:" << endl
<< "姓名:" << p->name //指针用 -> 访问
<< " 年龄:" << p->age
<< " 成绩:" << p->score << endl;
}
int main()
{
struct Student s; //创建结构体变量
s.name = "张三";
s.age = 16;
s.score = 76;
//创建结构体变量方法二: struct Student s = {"张三",16,76};
//调用打印学生信息的方法
printStudent1(s); //值传递
printStudent2(&s); //地址传递
cout << "在main函数中打印的结果:" << endl
<< "姓名:" << s.name
<< " 年龄:" << s.age
//值传递中,结果为16.没变
//地址传递中,结果为200.变了
<< " 成绩:" << s.score << endl;
system("pause");
return 0;
}
//第十章 程序的内存模型
// 内存四区的意义:不同区域存放的数据,有不同的生命周期
// C++中在'程序运行前'分为全局区和代码区. '程序运行前':双击运行exe程序前
//【代码区】:
// 存放程序中所有的二进制代码.会先将代码转成二进制, 再存放
// 特点是共享和只读.
// 共享:哪怕有的程序可以多次运行(多次双击exe运行),代码区中也只存放一份代码
// 只读: 用户只能运行代码,不能修改代码.如果可以修改,那直接在微信程序代码中将自己的零钱改到10000就好了
// 【全局区】:
// 存放 –全局变量、静态变量、常量区(常量区中存放: const修饰的全局常量 和 字符串常量)-
// 全局区的数据在程序结束后由操作系统释放.由操作系统来管理其死亡
// 【栈区】:
//【栈区】存放函数的参数(形参), 局部变量等
// 栈区的数据由编译器自动分配释放. 即由编译器来管理其生存和死亡
// 注意事项:不要返回局部变量的地址.
// 【堆区】:
// 在程序运行后,由程序员分配释放, 若程序员不释放, 程序结束时由操作系统回收. 即由程序员来管理其生存和死亡,有最迟死亡时间
// 在C++中主要利用new在堆区开辟内存. 语法:new 数据类型.
// 可一直存放.
// 【new 操作符】
// 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
// 10.1 全局区
//程序输出:全局区1518开头,常量区1517开头,栈区1631(和前两个区比较远)
int g_a = 10; //全局变量 ->全局区 地址为: 15187968
const int c_g_a = 10; //全局常量 ->常量区 地址为: 15178544
int main()
{
static int s_a = 10; //静态变量 ->全局区 地址为: 15187976
(int)&"hello world"; //字符串常量 ->常量区 (字符串常量:用双引号引起来的) 地址为: 15178740
int a = 10; //局部变量 ->栈区 地址为: 16317200
const int c_l_a = 10; //局部常量 ->栈区 地址为: 16317176
}
// 10.2 栈区
//局部变量存放在栈区,栈区的数据在函数执行完后由编译器自动释放(释放数据=释放内存).因此不要返回局部变量的地址!
//形参 也会放在栈区
int *func(int b) //形参b 也会放在栈区
{
b = 100;
int a = 10; //局部变量 .
return &a; //返回局部变量的地址
}
int main()
{
//利用指针来接受func函数的返回值.func函数的返回值为:局部变量a的地址
int *p = func(1);
cout << *p << endl; // 10. 第一次可以打印正确的数字,是因为编译器做了保留
cout << *p << endl; // 26799368(乱码). 第二次这个数据就已经被释放了,因此会乱码
system("pause");
return 0;
}
// 10.3 堆区
// new可将数据开辟到堆区: new 数据类型(初始值), 这个创建方式返回的是内存地址,因此要用指针来接收
int *func()
{
int *a = new int(10);
// new返回的是内存地址,因此要用指针来接收
//区分上个例子中的'int a = 10'创建的是局部变量
//指针本质上也是局部变量,放在栈区上. 指针保存的数据放在堆区.
return a;
}
int main()
{
int *p = func();
cout << *p << endl; // 10. 这个10会一直存活,除非按叉退出程序.或delete删除干净.
}
// 10.3.1 new的用法
//案例一:在堆区区利用new来建立整型数据
int *func()
{
int *p = new int(10); //在堆区建立整型数据.指针放在栈上
return p;
}
void test01() //有cout但是没有return,仍然用void
{
int *p = func();
cout << *p << endl; // 10. 存放在堆区
delete p; //堆区数据可通过delete释放
// cout << *p << endl; //报错
}
//案例二:在堆区利用new来创建数组
void test02()
{
int *arr = new int[10]; //返回的是连续内存空间组成的数组, 指针也可以是数组
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100; //给指针数组中的10个指针赋值为100-109
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl; //打印100-109
}
//释放堆区数组: 释放数组的时候, 要加[]来告诉编译器是对'整个数组'做释放
delete[] arr;
}
int main()
{
test01();
test02();
int *p = func();
cout << *p << endl;
}
//第十一章 引用
//引用: 给一个变量起一个别名. 给变量a取一个别名b,令变量a和b都能操控同一块内存.
// 11.1 引用的使用
//语法:数据类型 &别名=原名 .数据类型要和原来一样,不能变.
int main(int argc, char **argv)
{
int a = 10;
int &b = a; //创建引用
cout << "a= " << a << endl; // 10
cout << "b= " << b << endl; // 10
b = 100;
cout << "a= " << a << endl; // 100
cout << "b= " << b << endl; //输出都是100,因为a和b指向同一块内存
}
// 11.2 引用的注意事项
// 引用必须初始化
// 引用在初始化后不可改变
int main(int argc, char **argv)
{
int a = 10;
// int& b; //错误 ,引用创建的时候就必须赋值,进行初始化
int &b = a;
int c = 20;
// int& b = c; //错误. 引用初始化后不可以更改.b是a的别名,就不能改为是c的别名
b = c; //不能更改引用操作,可以进行赋值操作
cout << "a= " << a << endl; // 20
cout << "b= " << b << endl; // 20
cout << "c= " << c << endl; // 20
}
// 11.3 引用作为函数参数
// 作用:函数传参时,可以利用'引用'让形参修饰实参.(不需要指针,也可以达到地址传递的效果)
// 优点:简化指针修改实参
// 11.3.1 值传递
//形参不会修饰实参 = 形参改变,实参不发生改变
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
cout << "值传递中的a= " << a << endl; // 20
cout << "值传递中的b= " << b << endl; // 10
}
// 11.3.2 地址传递
//形参会修饰实参 = 形参改变,实参也发生改变
void Swap01(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
cout << "地址传递中的a= " << *a << endl; // 20
cout << "地址传递中的b= " << *b << endl; // 10
}
// 11.3.3 引用传递
// main函数中,调用的Swap02(a, b)将a传入函数,为其通过形参列表中的'int& a'取了别名. 原名和别名都是a,指向的是同一块内存
void Swap02(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
cout << "引用传递中的a= " << a << endl; // 20
cout << "引用传递中的b= " << b << endl; // 10
}
int main(int argc, char **argv)
{
int a = 10;
int b = 20;
// Swap(a, b); //值传递 a=10 b=20
// Swap01(&a, &b); //地址传递 a=20 b=10
Swap02(a, b); //地址传递 a=20 b=10
cout << "a= " << a << endl;
cout << "b= " << b << endl;
}
// 11.4 引用作为函数返回值
// 作用:引用可以作为函数的返回值存在的
// 注意:不要返回局部变量引用
// 用法:函数调用作为左值
// 1.不要return 局部变量的引用,会报错
int &test01()
{ //将局部变量a,以引用(int&)的形式,返还到外面
int a = 10; //创建局部变量,存放在栈区
return a;
}
// 2.函数调用可以作为左值. 左值是等号的左边, 即 函数名()=值
int &test02()
{
static int a = 10; //静态变量存放在全局区,程序结束后由系统释放
return a;
}
int main(int argc, char **argv)
{
// int& ref = test01(); // test01()函数返还出来的是 int&a, 此时相当于ref=a,打印ref就是打印a局部变量
//局部变量当函数执行完就自动释放了.
// cout << "ref = " << ref << endl; 10. 打印ref就是打印a局部变量.第一次能打出来,是因为编译器做了保留
// cout << "ref = " << ref << endl; 报错.a的内存已经被释放了
int &ref2 = test02(); // ref2=a
cout << "ref2 = " << ref2 << endl; // 10
cout << "ref2 = " << ref2 << endl; // 10
test02() = 1000; //如果函数的返回值是引用,这个函数调用可以作为左值
// int& ref2=值,是对引用赋值.即令ref2 =1000
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
}
// 11.5 引用的本质(了解即可,面试八股而已)
// 引用的本质在c++的内部实现是一个指针常量. 指针常量是指针指向不可改,这也说明为什么引用不可更改
// 推荐使用引用,语法方便
// 引用本质是指针常量,但是指针的操作编译器都自动帮我们转换了
void func(int &ref)
{ //引用会被自动转换为:int* const ref = &a; ref只能指向a的地址(0x0011),不能指向其他地址(如0x0022)
ref = 100; //引用会被自动转换为:*ref = 100; ref的值可以变
}
int main()
{
int a = 10;
int &ref = a; //引用会被自动转换为指针常量:int* const ref = &a;
ref = 20; //引用会被自动转换为:*ref = 20;
cout << "a:" << a << endl; // 20
cout << "ref:" << ref << endl; // 20 .这里的ref是指针,会被自动转换为:*ref
func(a);
cout << "a:" << a << endl; // 100
cout << "ref:" << ref << endl; // 100 .这里的ref是指针,会被自动转换为:*ref
}
// 11.6 常量引用
// 常量引用主要用来修饰形参,防止误操作
// 在函数形参列表中,可以加const修饰形参,防止形参改变实参
// 11.6.1 加const后编译器会自动对代码进行优化, 可对引用进行赋值
int main(int argc, char **argv)
{
int a = 10;
int &ref = a; //引用
// int &ref=10 报错 必须初始化
// const int& ref = 10; 正确
// 加了const相当于编译器将代码修改为 int temp=10; const int &ref=temp;
// ref = 20; //报错. 加了const之后变为只读,不可以修改
}
// 11.6.2 加const修饰形参,防止参数修改
void show(const int &val)
{
val = 1000; //报错,防止误操作
cout << "val= " << val << endl; // 100
}
int main(int argc, char **argv)
{
int a = 10;
show(a);
cout << "a= " << a << endl; // 100
}
// 第十二章 面向对象
// 面向对象三大特性: 封装, 继承, 多态.
// 万事万物皆对象: 人为类, 姓名年龄是属性, 跑吃饭唱歌是行为
// 12.1 封装
// 封装是C++面向对象三大特性之一
// 封装的意义:
// 将属性和行为作为一个整体,表现生活中的事物
// 将属性和行为加以权限控制
// 类中的属性和行为统一称为“成员”
// 属性 成员属性(成员变量)
// 行为 成员函数(成员方法)
// 12.1.1 意义一 将属性和行为作为一个整体,表现生活中的事物
// 语法: class 类名{ 访问权限: 属性 / 行为 };
//类中的成员属性和成员行为统一称为“成员”
//属性 = 成员属性 = 成员变量
//行为 = 成员函数 = 成员方法
// 实例化 : 通过一个类创建一个对象的过程
// 示例1:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号(两个行为)
class Student //设计类. 类后面紧跟着就是类的名称
{
public: //访问权限设为公共权限
//属性 —— 变量 —— 姓名,学号
string m_name;
int m_id;
//行为 —— 函数 —— 给姓名赋值, 给学号赋值 ,显示姓名和学号
void setName(string name)
{
m_name = name; //给姓名赋值
}
void setId(int id)
{
m_id = id; //给学号赋值
}
void showStudent()
{
cout << "姓名:" << m_name << "学号" << m_id << endl;
}
};
int main()
{
Student s1; //实例化——通过一个类创建一个对象的过程
s1.m_name = "张三" //给s1对象的属性进行赋值 方式一
s1.m_id = "1"
s1.setName("张三"); //给s1对象的属性进行赋值 方式二
s1.setId(1);
s1.showStudent(); //显示学生信息 方式一
cout << "学生信息为:" << s1.showStudent() << endl; //显示学生信息 方式二
}
// 12.1.2 意义二 访问权限
// 公共权限 public 类内可以访问 类外可以访问
// 保护权限 protected 类内可以访问 类外不可以访问 儿子可以访问父亲中的保护内容(如父亲的工作)
// 私有权限 private 类内可以访问 类外不可以访问 儿子不可以访问父亲中的保护内容(如父亲的银行卡)
//保护和私有权限的区别在继承中体现
// 没有设置访问权限的时候,则默认是私有private
class Person
{
public: //公共权限 - 姓名
string m_Name;
protected: //保护权限 - 汽车
string m_Car;
private: //私有权限 - 密码
int m_Password;
public: //公共函数 下面三个的赋值都属于类内,可以访问
void func1()
{
m_Name = "张三"; // m_Name是类内变量,可以访问
m_Car = "拖拉机";
m_Password = 123456;
}
private: //私有函数,类内可以访问,类外不可以访问
void func2()
{
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
}
}; //到此为止都是类内内容
int main()
{
Person p1; //实例化具体对象
//类外访问类内的变量和方法
p1.m_Name = "李四"; //类外可以访问,可以进行修改
// p1.m_Car = "奔驰"; //类外访问不到. 保护权限内容
// p1.m_Password = 123; //类外访问不到. 私有权限内容
p1.func1(); // public可以类外访问函数
// p1.func2(); //报错. private不可以类外访问
}
// 12.1.2.1 struct 与 class 的区别
// 在C++中 结构体struct和类class唯一的区别就在于 : 默认的访问权限不同
// struct 默认权限为公共
// class 默认权限为私有
class C1 //默认权限是私有private
{
int m_A;
};
struct C2 //默认权限是公共public
{
int m_A;
};
int main()
{
C1 c1;
// c1.m_A = 10; //无法访问,class默认权限是私有
C2 c2;
c2.m_A = 10; //可以访问,struct默认权限是公共
}
// 12.1.3 属性设置为私有,行为设置为公共!!
// 优点1: 将所有成员属性设置为私有,可以自己控制读写权限
// 优点2: 对于写权限,我们可以检测数据的有效性
// 分类: 又读又写; 只读; 只写
// 写:void set(参数类型 xxx) {m_x = xx;}
// 读:参数类型 get() {return m_x;}
//附加:在类中可以让另一个类 作为本类的成员. 见下方
// Person::setName()可表示 setName()是Person中的成员函数
//案例一: 设计人类
class Person
{
public:
void setName(string name) //设置姓名 写姓名
{
m_Name = name;
}
string getName() //获取姓名 读姓名
{
return m_Name; //返回string类型的字符串
}
//------------------------------------------------
int getAge() //获取年龄 只读
{
m_age = 20; // 初始化为20岁,这样的话最终的年龄还是20岁这样就是只读
return m_age; //返回int类型
}
//------------------------------------------------
void setLover(string lover) //设置女朋友 只写 —— 外界访问不到该数据
{
// if (m_Lover==none)
// {
// cout << "你这个单身狗,必须要有个对象!" << endl;
// return;
// } //优点2: 对于写权限,我们可以判断用户输入的数据有没有问题.通过if.
m_Lover = lover;
}
//将所有成员属性设置为私有,然后自己再控制读写权限
private:
string m_Name; //姓名 可读可写
int m_age; //年龄 只读 没有写内部写入函数
string m_Lover; //女朋友 只写 没有写内部读取函数
Student five; //在类中可以让另一个类 作为本类的成员. 这里是假设之前可能有一个class Student
};
int main()
{
Person p;
p.setName("张三"); //输入写姓名
cout << "姓名: " << p.getName() << endl; //输出读姓名
p.setAge(18); //报错. 年龄只能读,不能设置
cout << "年龄: " << p.getAge() << endl; //输出读年龄
p.setLover("刘亦菲"); //输入写女朋友
cout << "女朋友: " << p.getAge() << endl; //报错. 女朋友只能写,不能读
//本质上是'年龄'没有写内部写入函数; '女朋友'没有写内部读取函数
}
//案例二:设计立方体类(Cube),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等
//步骤:
// 1.创建立方体的类
// 2.设计属性(私有权限) 三个属性的设置(void set)与获取(int return)
// 3.设计行为 获取立方体面积和行为
// 4.判断两个立方体是否相等
// 方法一:成员函数 两个一比一
// 方法二:全局函数 一比二
//工程中经常使用头文件和源文件分写.
// 头文件.h 负责声明函数和变量.
// 源文件.cpp 负责实现函数&main程序.
class Cube
{
public: //行为
void setL(int l) //设置长
{
m_L = l;
}
int getL() //获取长
{
return m_L;
}
void setW(int w) //设置宽
{
m_W = w;
}
int getW() //获取宽
{
return m_W;
}
void setH(int h) //设置高
{
m_H = h;
}
int getH() //获取高
{
return m_H;
}
int calculateS() //获取立方体的面积
{
return 2 * m_L * m_W + 2 * m_W * m_H + 2 * m_L * m_H;
}
int calculateV() //获取立方体的体积
{
return m_H * m_L * m_W;
}
//方法一: 利用成员函数判断两个立方体是否相等
// bool isSameByClass(Cube& c) //利用引用传递的方式传递. 就不会再拷贝一份数据,且函数内外一致
// {
// //判断自身(c1)的长和传进来的长(c2,c3)进行比较
// if (m_L == c.getL() && m_H == c.getH() && m_W == c.getW())
// {
// return true;
// }
// return false;
}
private : //属性
int m_L; //长
int m_W; //宽
int m_H; //高
}; //以上是类内
//方法二: 定义全局函数. 利用全局函数判断 两个立方体是否相等
bool isSame(Cube & c1, Cube & c2)
{
if (c1.getL() == c2.getL() && c1.getH() == c2.getH() && c1.getW() == c2.getW()) //长宽高 均相等
{
return true;
}
return false;
}
int main()
{
Cube c1; //创建立方体对象
c1.setL(10);
c1.setH(10);
c1.setW(10);
Cube c2; //创建第二个立方体. 这里设置与c1完全相等
c2.setL(10);
c2.setH(10);
c2.setW(10);
Cube c3; //创建第三个立方体
c3.setL(20);
c3.setH(30);
c3.setW(40);
cout << "c1 的面积为:" << c1.calculateS() << endl;
cout << "c1 的体积为:" << c1.calculateV() << endl;
// 判断方法一: 利用成员函数判断 两个立方体是否相等
// bool ret2 = c1.isSameByClass(c2);//c1为已知立方体,再传入调用c2,进行对比
// if (ret2)
// {
// cout << "成员函数 c1 和 c2 相等" << endl;
// } else {
// cout << "成员函数 c1 和 c2 不相等" << endl;
// }
// bool ret3 = c1.isSameByClass(c3);//c1为已知立方体,再传入调用c3,进行对比
// if(ret3)
// {
// cout << "成员函数 c1 和 c3 相等" << endl;
// } else {
// cout << "成员函数 c1 和 c3 不相等" << endl;
// }
//判断方法二: 使用全局函数判断 两个立方体是否相等
bool ret1 = isSame(c1, c2);
if (ret1)
{
cout << "全局函数 c1 和 c2 相等" << endl;
}
else
{
cout << "全局函数 c1 和 c2 不相等" << endl;
}
}
// 12.2. 对象的初始化和清理
// 12.2.1 构造函数(初始化)和析构函数(清理)
// 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
// C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置
// 构造函数和析构函数如果你自己不写,编译器就会自动调用一个空实现的构造和析构函数(没有代码). 你写了就用你的,你没写就用编译器的
// 1. 构造函数(写作用域),进行初始化操作
//语法:类名(){}
//函数名与类名相同
//有返回值,不用写void
//构造函数可以有参数,可以发生重载
//创建对象时,构造函数会自动调用,而且只调用一次
// 2. 析构函数 进行清理操作
//语法: ~类名(){}
//函数名和类名相同,在名称前加~
//没有返回值 不写void
//析构函数不可以有参数,不可以发生重载
//对象在销毁前,会自动调用析构函数,而且只会调用一次
class Person
{
public: //成员函数
Person() // 1. 构造函数(写作用域),进行初始化操作.由于我们自己写了构造函数,当实例化对象的时候,执行的是我们这个构造函数
{
cout << "Person 构造函数的调用" << endl;
}
~Person() // 2. 析构函数 进行清理操作.释放对象前就会自动调用这个我们写好的析构函数.
{
cout << "Person 析构函数调用" << endl;
}
}; //以上是类内
void test01()
{
Person p1; //实例化person时会调用我们写好的构造函数.
// p1是局部变量,在栈上的数据,test01执行完毕后自动释放这个对象. 释放对象前就会自动调用我们写好的析构函数.
}
int main()
{
//在test01()函数内实例化了一个对象p1
test01(); //结果: "Person 构造函数的调用" "Person 析构函数调用".
cout << endl;
//又在全局实例化了一个对象p2是全局变量, main程序执行完,按0退出main程序时才会 自动调用我们写好的析构函数
Person p2; //结果: "Person 构造函数的调用" . 实例化person时会调用我们写好的构造函数.
system("pause");
return 0
}
// 12.2.2 构造函数的分类及调用
//构造函数有两种分类:
// 1 按构造函数是否有参数 - 无参构造, 有参构造
// 2 按类型 - 普通构造, 拷贝构造!!! 加引用
//构造函数有三种调用方式:
// 3 括号法(推荐)
// 4 显示法
// 5 隐式转换法
class Person
{
public:
Person()
{
cout << "Person 无参构造函数的调用" << endl; //无参构造函数
} //系统自带的是无参的空构造函数
Person(int a)
{
age = a;
cout << "Person 有参构造函数的调用" << endl; //有参构造函数
}
Person(const Person &p) //拷贝构造函数 //拷贝一模一样的数据出来
{
//将传入的人身上所有属性,拷贝到我身上,并且之后不能对原人物信息进行二次修改,因此是const
age = p.age;
}
~Person() //析构函数调用
{
cout << "Person 析构函数调用" << endl;
}
int age;
}; //类内
//调用(三种方法)
void test01()
{
// 1.括号法
//注意:调用默认构造函数时,不要加小括号().如果写成Person p(),编译器会认为是函数的声明,如void func().不会认识是在创建对象,不运行构造函数
Person p; //默认构造函数 调用
Person p0(10); //有参构造函数
Person p1(p0); //拷贝构造函数. 将p0的所有属性拷贝给p1.
cout << "p0 年龄为: " << p0.age << endl; //显示为10
cout << "p1 年龄为: " << p1.age << endl; //显示为10
cout << endl;
// 2.显示法
Person p6; //默认构造
Person p2 = Person(20); //有参构造. 对象名称写在'等号右边'
Person p3 = Person(p2); //拷贝构造
cout << "p2 年龄为: " << p2.age << endl; //显示为20
cout << "p3 年龄为: " << p3.age << endl; //显示为20
cout << endl;
// Person(10); //这一行执行完毕,会立刻析构对象.然后才执行下一行. 运行结果见下方
// cout << "aaaaa" << endl;
// "Person 有参构造函数的调用"
// "Person 析构函数调用"
// "aaaaa"
//匿名对象特点:当前行执行结束后,系统会立即回收匿名对象. 匿名对象指经过实例化创建,但是不在'等号右边',因此该对象没有名字.
// Person(p3); //会报错,重复定义.
//不要利用拷贝函数初始化匿名对象,编译器会将Person (p3) 认为是 Person p3 (隐式转换). 括号被自动去掉后,就是对象声明了
// 3.隐式转换法
Person p7; // 默认构造
Person p4 = 30; //相当于 Person p4 = Person(10);有参构造
Person p5 = p4; //拷贝构造
cout << "p4 年龄为: " << p4.age << endl; //显示为30
cout << "p5 年龄为: " << p5.age << endl; //显示为30
cout << endl;
} //函数内
int main()
{
test01();
}
// 12.2.3 什么时候会调用拷贝构造函数? (3种情况)
// 1 使用已经创建完毕的对象来初始化一个新对象
// 2 值传递的方式给函数参数传值
// 3 以值方式返回局部对象(难点)
class Person
{
public:
Person() //无参构造函数
{
cout << "Person 默认构造函数调用" << endl;
}
Person(int age) //有参构造函数
{
cout << "Person 有参构造函数调用" << endl;
m_age = age;
}
Person(const Person &p) //拷贝构造函数
{
cout << "Person 拷贝构造函数调用" << endl;
m_age = p.m_age;
}
~Person() //析构函数
{
cout << "Person 析构函数调用" << endl;
}
int m_age;
};
// 1.使用一个已经创建完毕的对象来初始化一个新对象 Person p2(p1);
void test01()
{
Person p1(20); // 有参构造
Person p2(p1); // 拷贝构造函数调用
cout << "p2 年龄为:" << p2.m_age << endl; // 假设下面的代码都注释化了,即程序运行到此为止.则:
// "Person 默认构造函数调用"
// "Person 默认构造函数调用"
// 20
// "Person 析构函数调用"
// "Person 析构函数调用"
}
// 2.值传递的方式给函数参数传值 doWork(p3);
void doWork(Person p)
{
p.age = 1000; // 值传递, 拷贝出临时的副本,不会影响下面的数据
}
void test02()
{
Person p3; // 无参构造函数调用
doWork(p3); // 实参p3传给doWork函数的形参Person p时, 会调用拷贝构造函数
}
// 3.值传递的方式返回局部对象. Person p = doWork2();
Person doWork2() // doWork2函数返还出去的p1是Person类型. 因为进行的是值传递, p1会拷贝一个新的对象,返回给test03
{
Person p1; // 局部对象,调用默认构造函数.局部对象 函数执行完之后,就会被释放掉
return p1;
cout << (int *)&p1 << endl;
}
void test03()
{
Person p = doWork2(); // 创建局部对象
cout << (int *)&p << endl; // p 和 p1不是一个
}
int main()
{
test01();
cout << endl;
test02();
cout << endl;
test03();
}
// 12.2.4 构造函数调用规则
// 1. 创建一个类,C++编译器会给每个类都添加至少3个函数
//①默认构造 (空实现) < 有参构造函数
//②拷贝函数 (值拷贝).
//③析构函数 (空实现)
// 2. 如果我们写了有参构造函数,编译器就不再提供默认无参构造.'有高就无低': 默认构造 (空实现) < 有参构造函数''
// 如果我们没写拷贝构造函数,编译器也会自动地帮我们写一个仅是值拷贝的拷贝构造函数.有①仍有②
class Person
{
public:
Person(int age) //有参构造函数
{
m_age = age;
cout << "Person 有参构造函数调用" << endl;
}
int main()
{
// Person p1; //报错 . 无默认构造函数
Person p1(18); //有参构造函数
Person p2(p1); //成功. 拷贝构造函数调用. 如果我们没写拷贝构造函数,编译器也会自动地帮我们写一个仅是值拷贝的拷贝构造函数
cout << "p2 年龄为: " << p2.m_age << endl; // 18
}
}
// 3. 如果我们只写了拷贝构造函数,但是没写其他普通(无参+有参)构造函数,编译器也不会再提供其他普通(无参+有参)构造函数了.
// 只要用户写了“高级”的构造函数,“低级”的就不再提供了.'有高就无低':有②则无①
class Person
{
public:
Person(const Person &p) //拷贝构造函数
{
m_age = p.m_age;
cout << "Person 拷贝构造函数调用" << endl;
}
int main()
{
// Person p1; //报错 . 无默认构造函数
// Person p1(18); //报错 . 无有参构造函数
}
}
// 12.2.5 深拷贝和浅拷贝(难点)
// 浅拷贝:简单的赋值拷贝操作.
// 浅拷贝场景:自己没有写拷贝构造函数,编译器会自动提供一个拷贝构造函数,做浅拷贝操作。就是程序提供的等号赋值工作
// 浅拷贝带来的问题是:堆区的内存重复释放.先被p1释放,再被p2释放.
// 浅拷贝问题用深拷贝解决
// 深拷贝:在堆区重新申请空间,进行拷贝操作.深拷贝指针指向的堆区是不同的,在做释放时就不会出现错误。
// 如果属性有在堆区开辟的(new),一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
//浅拷贝
class Person
{
public:
Person(int age, int height)
{
cout << "Person 有参构造函数调用" << endl;
m_age = age;
m_height = new int(height);
// 利用new把身高创建在堆区,用指针接收堆区的数据. 堆区数据由程序员用delete可将其释放,delete写在下方析构函数中
// m_height是指针,里面存放的值是一串内存地址
// p1和p2两个对象由于是浅拷贝,对象属性的值和内存地址都一样.在释放的时候,先释放p2,再释放p1的时候,会遇到同一块内存地址重复释放的问题.会报错!!
}
// 析构函数 —— 作用是将堆区开辟的数据做释放操作
~Person()
{
cout << "析构函数调用" << endl;
if (m_height != NULL)
{
delete m_height; // 利用delete释放干净
m_height = NULL; // 防止野指针出现,置空操作. 可省略这一步?
}
}
int m_age; // 年龄
int *m_height; // 身高用指针,目的是把身高的数据开辟到堆区
};
void test01() // 栈的规则是先进后出,所以先释放p2,再释放p1.在释放p1的时候,会遇到同一块内存地址重复释放的问题.
{
Person p1(18, 180);
cout << "p1 年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl; // m_height是个指针,因此打印要解引用
Person p2(p1); // 由于没有自己写的拷贝构造函数,这里会自动用编译器提供的拷贝构造函数,会做浅拷贝操作(值传递)
cout << "p2 年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
// p1和p2由于是浅拷贝,值和内存地址都一样.在释放的时候,先释放p2,再释放p1的时候,会遇到同一块内存地址重复释放的问题.
}
int main()
{
test01(); // test01()执行完了,就可以释放p2,p1,销毁堆区数据
system("pause");
return 0;
}
//深拷贝: 写自己的拷贝构造函数即可.
class Person
{
public:
Person(int age, int height) // 参数构造函数不变
{
cout << "Person 有参构造函数调用" << endl;
m_age = age;
m_height = new int(height);
}
// 自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person &p)
{
cout << "Person 拷贝构造函数调用 " << endl;
m_age = p.m_age;
// m_height = p.m_height; //这是浅拷贝代码,编译器默认实现的代码就是这个
m_height = new int(*p.m_height); // 解引用为18, 重新在堆区为这个18申请一块内存,返还新的内存地址
}
~Person() // 析构函数不变
{
cout << "析构函数调用" << endl;
if (m_height != NULL)
{
delete m_height;
m_height = NULL;
}
}
int m_age;
int *m_height;
};
void test01()
{
Person p1(18, 180);
cout << "p1 年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl; // m_height是个指针,因此打印要解引用
Person p2(p1); // 用自己写的拷贝构造函数,做深拷贝操作
cout << "p2 年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}
int main()
{
test01(); // test01()函数执行完,p1走p1的析构,p2走p2的析构,没有重复释放的问题
system("pause");
return 0;
}
// 12.2.6 初始化列表
// C++提供了初始化列表语法,用来初始化属性
// 语法:构造函数():属性m_A(值a),属性m_B(值b),属性m_C(值c)... {}
// Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
class Person
{
public:
// 传统方式初始化 利用构造函数初始化
// Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
//}
//初始化列表方式初始化
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
void PrintPerson()
{
cout << "m_A = " << m_A << endl;
cout << "m_B = " << m_B << endl;
cout << "m_C = " << m_C << endl;
}
private:
int m_A;
int m_B;
int m_C;
};
int main()
{
Person p(1, 2, 3);
p.PrintPerson();
}
// 12.2.7 类对象作为类成员
// C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员.
//语法:
// class A {}
// class B // B类中有对象A作为成员
// {
// A a; //A为对象成员, a是成员属性
// }
//构造的顺序是 :先调用对象成员的构造,再调用本类构造
//析构顺序与构造相反.原因: 栈区数据 先进后出.
class Phone // 手机类 —— 人这个类的成员
{
public:
Phone(string pName) // 有参构造函数
{
cout << "Phone 有参构造函数调用" << endl;
m_PName = pName;
}
~Phone()
{
cout << "Phone 析构函数调用" << endl;
}
string m_PName; // 手机品牌名称
};
class Person // 人类
{
public:
Person(string name, string pName) : m_Name(name), m_Phone(pName)
{
cout << "Person 构造函数调用" << endl;
}
~Person()
{
cout << "Person 析构函数调用" << endl;
}
string m_Name; // 姓名
Phone m_Phone; // Phone是成员类=对象成员. m_Phone是成员变量.
// 上面的初始化列表实际上就是让这里的 Phone m_Phone = pName, 属于用隐士转换的方法调用构造函数.
};
void test01()
{
Person p("张三", "苹果MAX"); //将"张三"传入name,将"苹果MAX"传入pName. 给属性赋值: m_Name="张三", m_Phone="苹果MAX".
cout << p.m_Name << "拿着" << p.m_Phone.m_PName << endl; // m_Phone属性是成员类Phone的实例化. 成员类Phone中另有m_PName属性.
// 当其他类对象作为本类成员,构造时先构造其他类对象(先phone再person),再构造自身。析构的顺序与构造相反
}
int main()
{
test01();
// Phone的构造函数调用
// Person的构造函数调用
//张三拿着: 苹果MAX
// Person的析构函数调用
// Phone的析构函数调用
}
// 12.2.8 静态成员(成员变量和函数加static,重点难点)
// 12.2.8.1 静态成员变量
// 静态成员变量特点:
// 1 在编译阶段分配内存(全局区)(编译阶段:双击运行程序前)
// 2 类内声明,类外初始化. 类外调用的时候必须为其设置初始值.
// 3 所有对象共享同一份数据. 所有Person的静态变量信息都是相同的.
// 静态成员变量两种访问方式
// 1、通过对象 访问静态成员变量. p.m_A
// 2、通过类名 访问静态成员变量. Person::m_A
class Person
{
public:
static int m_A; // 静态成员变量的类内声明.
private:
static int m_B; // 静态成员变量也是有访问权限的,设置为private的static,类外不可以访问
}; //类内
int Person::m_A = 100; // 静态变量的类外初始化
int Person::m_B = 300; // 报错. 设置为private的静态变量类外不可以访问
void test01()
{
// 1、通过对象 访问静态成员变量
Person p;
cout << p.m_A << endl; // 100
Person p2;
p2.m_A = 200;
cout << p.m_A << endl; // 200 .因为所有对象共享同一份数据
// 2、通过类名 访问静态成员变量
cout << "m_A = " << Person::m_A << endl; // 200
// cout << "m_B = " << Person::m_B << endl; // 私有权限访问不到
}
int main()
{
test01();
system("pause");
return 0;
}
// 12.2.8.1 静态成员函数
// 静态成员函数特点
// 1 所有对象共享同一个函数
// 2 静态成员函数只能访问静态成员变量(要改就全改), 非静态成员变量无法访问(不能针对某一个对象改).
// 静态成员函数两种访问方式
// 1. 通过对象进行访问. p.func();
// 2. 通过类名进行访问. Person::func(); 这种方式不用实例化对象
class Person
{
public:
static void func() // 静态成员函数
{
m_A = 100; // 静态成员函数可以访问 静态成员变量(共享). (要改就全改)
// m_B = 200; // 静态成员函数 不可以访问 非静态成员变量,无法区分到底是哪个对象的m_B的属性.(不能针对某一个对象改)
cout << "static void func 调用" << endl;
}
static int m_A; // 静态成员变量
int m_B; // 非静态成员变量
private: // 静态成员函数也是有访问权限的,私有不可访问
static void func2()
{
cout << "static void func2 调用" << endl;
}
}; //类内
int Person::m_A = 10; // 静态成员变量的类外初始化. 这里是变量!不是函数! 函数不需要初始化.
// int Person::m_B = 10;
void test01()
{
// 静态成员函数两种访问方式
// 1. 通过对象进行访问
Person p;
p.func();
// 2. 通过类名进行访问
Person::func();
// Person::func2();//类外访问不到私有的静态成员函数
}
int main()
{
test01();
system("pause");
return 0;
}
// 第十三章 对象模型和this指针
// 13.1 成员变量和成员函数分开存储
//空对象占用的内存空间为: 1; 只有非静态成员变量才属于类的对象上,其他的大家共享的是一份数据
// 13.1.1 空对象
// C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
// 每个空对象也应该有个独一无二的内存地址(占坑行为)
// 空对象占用的内存空间为: 1字节
// 空对象:类里面是空的,没有属性和成员.此时类实例化的对象就是空对象
// 空对象. // 空对象占用的内存空间为: 1
class Person
{
}; //类里面是空的,没有属性和成员.此时类实例化的对象就是空对象
void test01()
{
Person p;
// 空对象占用的内存空间为: 1
cout << "size of p=" << sizeof(p) << endl;
}
int main()
{
test01(); // 1个字节
system("pause");
return 0;
}
// 13.1.2 非空对象. 成员变量和成员函数分开存储.只有非静态成员变量才属于类的对象上,其他的大家共享的是一份数据
class Person
{
int m_A; // 非静态成员变量,属于类的对象上
static int m_B; // 静态成员变量 —— 需要类内声明,类外初始化,不属于类的对象上. 大家共享的是一份数据
void func() {} // 非静态成员函数,不属于类的对象上. 大家共享的是一份数据,只是函数里面有方式能确认是谁在调用该非静态函数-this指针.
static void func2() {} // 静态成员函数, 不属于类的对象上,大家共享的是一份数据
}; //类非空
int Person::m_B = 10;
void test02()
{
Person p1; // 整个类中只有非静态成员变量int m_A在类的对象上,int空间大小为4个字节,因此实例化的时候,只开辟4个字节的空间.
cout << "size of p1 =" << sizeof(p1) << endl;
}
int main()
{
test02(); // 4个字节
system("pause");
return 0;
}
// 13.2 this指针
// 在C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象p1,p2,p3会共用一块代码。
// 问题是:这一块代码是如何区分那个对象调用自己的呢?就是说 假设只有一个函数,对象p1,p2,p3都在使用这个函数,在函数体内部如何区分是p1调的还是p2调的?应该相应修改p1还是p2的属性呢?
// this指针[指向]被调用的成员函数所属的[对象]. p1调用成员函数,this就指向p1;p2调用成员函数,this就指向p2,以此类推.
// this指针是包含在每一个非静态成员函数内部的一种指针.
// this指针不需要定义,直接使用即可
// this指针的用途:
// 当形参和成员变量同名时,可用this指针来区分
// 在类的非静态成员函数中返回对象本身,可使用return *this. 因为this本质上是个指针, *this是在解引用.
// this指针的本质 是指针常量 指针的指向是不可以修改的
// 13.2.1 解决冲突问题
//名称冲突,会报错:
class Person
{
public:
Person(int age) //这是形参age
{
age = age; // 报错.如果成员变量和传入形参的名称一致,编译器会觉得这是一个age,即会将四个age看做一个age
}
int age;
// 解决冲突问题方法一 :使用m_,区分和形参的名称
public:
Person(int age) //这是形参age
{
m_age = age; // 成员英文member,m_age代表是个成员变量. //形参age来给成员变量m_age赋值.
}
int m_age;
// 解决冲突问题方法二 :使用this指针(推荐)
// p1 在调用有参构造函数,所以this指向被调用的成员函数所属的对象
public:
Person(int age) //这是形参age
{
this->age = age;
// this指针指向 被调用成员函数 所属的对象. p1调用成员函数,this就指向p1;p2调用成员函数,this就指向p2,以此类推.
// this指的是成员属性age,后面是形参age
}
int age;
} //类内
void
test01()
{
Person p1(10);
cout << "p1的年龄为:" << p1.age << endl;
}
// 13.2.2 返回对象本身用*this
class Person
{
public:
// 如果开头Person&变成Person的话,会变成值传递,Person就会创建和本体不一样的新数据,调用拷贝构造函数(用值得方式返回,会复制一份新的数据,Person和*this自身是不一样的数据)。通过一次次链式传递,新生成不同于p2的新的对象,每次返回的都是新的对象。
Person &PersonAddAge2(Person &p) // 第2个Person&表示以引用的方式传入 // 第1个Person&表示以引用的方式返回.
{
this->age += p.age; // 将别人的年龄加到自身上面
return *this; // this指向p2的指针,*this指向的就是p2这个对象的本体
// 要返回本体,要用引用的方式返回,不能只用值返回Person
}
} //类内
void
test01()
{
Person p1(10);
Person p2(10);
Person p2.PersonAddAge2(p1); // 20. 将p1的年龄加到p1身上.
p2.PersonAddAge2(p1).PersonAddAge2(p1).PersonAddAge2(p1); // 40 . 将p1的年龄加三次到p1身上.
//用Person& 引用返回,则Person p2.PersonAddAge2(p1)返回的结果是p2;然后p2又可以进一步调用第二个PersonAddAge2(p1),又返回p2出来;然后p2又可以进一步调用第三个PersonAddAge2(p1),又返回p2出来
// 如果开头Person&变成Person的话,会变成值传递,Person就会创建和本体不一样的新数据.结果是p2只会变成20,即只会加一次,剩下的两个10加到了副本身上
cout << "p2的年龄为:" << p2.age << endl; // 40
}
// 13.3 空指针访问成员函数
// C++中空指针也是可以调用成员函数的,但是也要注意成员函数中有没有用到this指针. 如果用到this指针,需要加以判断保证代码的健壮性,使用下面程序
//函数中的this指针不能指向空指针,否则会报错.
//报错情形:
class Person
{
public:
void showPersonName()
{
cout << "age = " << this->m_Age << endl;
}
int m_Age;
};
void test01()
{
Person *p = NULL; //创建空指针
p->showPersonName(); //空指针调用成员函数. 报错.因为函数中的this此时指向了一个空指针,'this->m_Age '
Person p1;
p1.showPersonName();
}
int main()
{
test01();
}
//改错: 修改类中void showPersonName()函数
class Person
{
public:
void showPersonName()
{
// 改错. 报错的原因是传入的指针为空NULL,加入if后没法无中生有
if (this == NULL)
{
return;
}
cout << "age = " << this->m_Age << endl;
}
int m_Age;
};
void test01()
{
Person *p = NULL; //创建空指针
p->showPersonName(); //空指针调用成员函数. 报错.因为函数中的this此时指向了一个空指针,'this->m_Age '
Person p1;
p1.showPersonName();
}
int main()
{
test01();
}
// 13.4 const 修饰成员函数(常函数 常对象)
// 13.4.1 常函数:
// 成员函数后加const的函数为常函数,常函数内不可以修改成员属性 void showPerson() const; this->m_A = 100;
// 成员属性声明时加关键字mutable后,在'const常函数'中仍然可以修改值 mutable int m_B; this->m_B = 100;
class Person
{
public:
void showPerson() const //将showPerson()函数变为常函数
{
// this指针的本质 是指针常量 指针的指向是不可以修改的 但指针指向的值可以修改
// 在成员函数后面加const,修饰的是this指针,让指针指向的值从可以修改 变为不可以修改
// this = NULL; //报错. 指针常量 指针的指向是不可以修改的,p调用的,就指向p
this->m_A = 100; //报错. //成员函数后加const的函数为常函数,属性值就不可以修改
this->m_B = 100; //不报错. //成员属性声明时加关键字mutable后,在const常函数中仍然可以修改值
}
void func()
{
m_A = 500
}
int m_A;
mutable int m_B; //将m_B变量变为 可变变量
};
void test01()
{
Person p;
p.showPerson();
}
// 13.4.2 常对象:
// 声明对象前加const,为常对象. 常对象的属性也不可以修改 const Person p; p.m_A = 100;
// 成员属性声明时加关键字mutable后,在'const常对象'中仍然可以修改值 mutable int m_B; p.m_B = 100;
// 常对象只能调用常函数 void showPerson() const ; p.showPerson();
// 常对象不能调用普通函数,因为普通成员函数可以修改属性. void showPerson() const ; p.func();
void test02()
{
const Person p; // 将p变为常对象
// p.m_A = 100;// 报错. 对象属性不可以修改
p.m_B = 100; // m_B是可变变量,在常对象下也可以修改(加上mutable)
p.showPerson(); // 常对象只能调用常函数
// p.func();// 常对象不可以调用普通的成员函数,因为普通成员函数可以修改属性,如m_A = 500, 这就侧面地把不能修改的属性修改了
}
int main()
{
system("pause");
return 0;
}
// 13.5 友元(便于访问私有函数)
// 生活中你的家(Building)有客厅(Public),有你的卧室(Private),客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是你也可以允许你的好闺蜜好基友进去。
// 在程序里,有些私有属性,想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术,友元的目的就是让一个函数或者类 访问另一个类中私有成员,友元的关键字为 friend
// 13.5.1 全局函数做友元(一个全局函数可以访问另一个类的私有成员)
//方式:在被访问类中的开头加入一条friend+全局函数声明后,该全局函数就成了类(Building)的好朋友. 该全局函数可以访问该类的实例(building1)的私有属性.
// friend void goodGay(Building* building);
//案例:让一个全局函数goodGay可以访问另一个类Building的私有成员
// 类(Building)
class Building
{
//方式:在类开头中加入一条friend+全局函数声明后,该全局函数就成了类(Building)的好朋友. 该全局函数可以访问该类的实例(building1)的私有属性.
friend void goodGay(Building *building);
public:
Building() //默认构造函数
{
m_SittingRoom = "客厅"; //为属性赋予初值
m_BedRoom = "卧室";
}
string m_SittingRoom; // 客厅
private:
string m_BedRoom; // 卧室. 设置为私有属性.
};
// 全局函数
void goodGay(Building * building) // 传进一个实例化对象. 这里是传入指针Building(引用也可以)
{
cout << "好基友的全局函数 正在访问:" << building->m_SittingRoom << endl; // 公共属性
cout << "好基友的全局函数 正在访问:" << building->m_BedRoom << endl; // 私有属性. //好基友的全局函数 正在访问:卧室
}
void test01()
{
Building building1; // 实例化Building对象
goodGay(&building1); // 以指针的形式(传入地址)将对象传入全局函数
}
int main()
{
test01();
system("pause");
return 0;
}
// 13.5.2 类做友元(一个类的实例化对象可以访问另一个类的私有成员)
// 案例:让一个类(goodGay)的实例化对象可以访问另一个类(Building)的私有成员m_settingRoom.
//方式:
//先分清 被访问类,访问类
// 在被访问类Building中的开头加入一条friend+访问类声明 friend class GoodGay;
// 程序运行步骤:
// 创建goodGay对象gg,创造对象时会调用goodGay的构造函数
// GoodGay::GoodGay()写在类外,使用new Builing 创建一个building对象,创造对象时会调用building的构造函数
// Building::Building(),里面已经赋好初值了。
// gg.visit() 调用visit函数,可以访问building内部维护的m_settingRoom
// friend class goodGay:可以让goodGay实例化对象访问Building里面的私有属性
class Building; // 创建building,告诉编译器一会会写这个类
class GoodGay // 创建goodgay类
{
public:
GoodGay(); // 无参构造函数. //类外写构造函数,见下方.
void visit(); // 参观函数 用于访问Building中的属性 公共和私有属性.// 类外写成员函数,见下方.
private:
Building *building; // 指针类型
};
class Building
{
friend class GoodGay; //可以让goodGay实例化对象访问Building里面的私有属性
public:
Building(); // 无参构造函数,构造函数用于 赋初值. // 类外写构造函数,见下方.
public:
string m_SittingRoom; // 客厅
private:
string m_BedRoom; // 卧室
};
// 类外写Building成员函数
Building::Building() // 类中的成员函数,Building下面的构造函数Building,赋初值
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
// 类外写GoodGay成员函数1
GoodGay::GoodGay()
{
building = new Building; // 创建建筑物对象
// new表示在堆区创建出一个对象,new什么样的类型,就返回什么样的指针
}
// 类外写GoodGay成员函数2
void GoodGay::visit()
{
cout << "好基友类正在访问:" << building->m_SittingRoom << endl;
cout << "好基友类正在访问:" << building->m_BedRoom << endl; //
}
void test01()
{
GoodGay gg; // 创建goodGay对象,调用goodGay的构造函数
gg.visit();
}
int main()
{
test01();
system("pause");
return 0;
}
// 13.5.2 成员函数做友元
//让GoodGay类中的成员函数visit1() 可以访问Building中私有成员m_settingRoom
//方式:
//先分清 被访问类,访问类
// 在被访问类Building中的开头加入一条friend+访问类成员函数声明 friend void GoodGay::visit1();
// 程序运行步骤:
// 创建goodGay对象gg,创造对象时会调用goodGay的构造函数
// GoodGay::GoodGay()写在类外,使用new Builing 创建一个building对象,创造对象时会调用building的构造函数
// Building::Building(),里面已经赋好初值了。
// gg.visit1() 调用visit1函数,可以访问building内部私有的m_settingRoom
// gg.visit2() 调用visit2函数,不可以访问building内部私有的m_settingRoom
class Building; // 首先声明Building类
class GoodGay
{
public:
GoodGay(); // 无参构造函数 // 用类外实现函数
void visit1(); // 让visit1函数可以访问Building中私有成员
void visit2(); // 让visit2函数不可以访问Building中私有成员
Building *building1;
};
class Building
{
// 告诉编译器,GoodGay类下的visit1成员函数作为本类的好朋友,可以访问私有成员
friend void GoodGay::visit1();
// friend void GoodGay::visit2();// 没有这句的话visit2不可以访问私有成员
public:
Building(); // 构造函数的声明
public:
string m_SittingRoom; // 客厅
private:
string m_BedRoom; // 客厅
};
// 类外实现成员函数
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay() // 类外实现
{
building1 = new Building;
// new一个Building对象,返回的是building1指针
}
void GoodGay::visit1()
{
cout << "visit1 函数正在访问:" << building1->m_SittingRoom << endl;
cout << "visit1 函数正在访问:" << building1->m_BedRoom << endl;
}
void GoodGay::visit2()
{
cout << "visit2 函数正在访问:" << building1->m_SittingRoom << endl;
// cout << "visit2函数正在访问:" << building1->m_BedRoom << endl; //报错. 成员函数不做友元,不能访问私有成员
}
void test01()
{
GoodGay gg;
gg.visit1();
gg.visit2();
}
int main()
{
test01();
system("pause");
return 0;
}
// 第十四章 运算符重载
// 运算符重载包括 成员函数重载 或者全局函数重载 两种方式.
// 将系统预设的运算符,用于用户自定义的数据类型,就是运算符重载.
// 如系统有加号,可以让两个数值相加,但不能让两个对象相加. 运算符重载 可以让 3=0+1 ->> Person p3 = p0 + p1
//语法: 返回类型 operator重载符号(参数表){重载函数体}
// Person operator+ (Person& p) {...}
// 运算符重载的实质是函数重载.
// 函数重载满足条件:
// 同一个作用域(比如都是成员函数)
// 函数名一样operator
// 参数不一样,个数,类型,顺序不一样
// 返回值不作为重载条件
// 14.1 加号运算符重载
// 总结1:对于内置的数据类型的表达式的的运算符是不可能改变的
// 总结2:不要滥用运算符重载
// 就是相当于重新定义了一个“+”,你可以用加号来做你想做的事,比如在里面写一个交换函数
// 14.1.1 成员函数重载
class Person
{
public:
// 方式1、成员函数重载+号, this指的是正在调用的对象,这里this指向的就是p0,p1
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;
}
int m_A;
int m_B;
}; //类内
void main()
{
Person p0;
p0.m_A = 10;
p0.m_B = 10;
Person p1;
p1.m_A = 20;
p1.m_B = 20;
Person p2 = p0.operator+(p1); // 调用方式一 . 成员函数重载的本质
// Person p3 = p0 + p1; // 调用方式二. 全局函数重载和成员函数重载均可以使用这种方式
cout << "p2.m_A = " << p2.m_A << endl;
cout << "p2.m_B = " << p2.m_B << endl;
}
// 14.1.2 全员函数重载
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;
}
void main()
{
Person p0;
p0.m_A = 10;
p0.m_B = 10;
Person p1;
p1.m_A = 20;
p1.m_B = 20;
Person p2;
p2 = operator+(p0, p1); // 全局函数重载的本质
// p2 = p0 + p1; // 全局函数重载和成员函数重载均可以使用这种方式
cout << "p2.m_a:" << p2.m_a << endl;
cout << "p2.m_b:" << p2.m_b << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
// //14.1.3 函数重载+
// 运算符重载的实质是函数重载.
class Person
{
public:
int m_A;
int m_B;
}; //类内
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 test01()
{
Person p0;
p0.m_A = 10;
p0.m_B = 10;
Person p1;
p1.m_A = 20;
p1.m_B = 20;
Person p4 = p0 + 100; // Person + int
cout << "p4.m_A = " << p4.m_A << endl;
cout << "p4.m_B = " << p4.m_B << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
// 14.2 左移<<运算符重载(可输出自定义数据类型)
//左移运算符重载 配合友元可以实现输出自定义数据类型
// 14.2.1 利用成员函数重载左移运算符
// 结论:成员函数重载的结果为p.operator<<(cout) 简化版本 p << cout,无法实现 cout 在左侧. 因此只能利用全局函数重载左移运算符
class Person
{
public:
//利用成员函数重载左移运算符
void operator<<(Person &p)
//报错. p.operator<<(p)不成立
//尝试改为 p.operator<<(cout), 借鉴上一节讲的,简化版的相当于把.operator和<<(需要重载的运算符)省略掉.
//由于cout本质上是ostream类的对象, 就会变成 ' p << cout ', 也不符合要求.无法实现 cout 在左侧
//根据观察,发现上一节中的全局函数重载,更能够将两个对象按顺序用重载运算符连接起来,故决定用全局函数重载.全局函数重载的实现方式见下方.
int m_A;
int m_B;
};
void test01()
{
// 方式1
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p.m_A << endl;
cout << p.m_B << endl;
cout << p; // 要通过打印p 能够完整打印出p.m_A 和p.m_B,即上两行一样的效果
}
int main()
{
test01();
system("pause");
return 0;
}
// 14.2.2 利用全局函数重载左移运算符
// 14.2.2.1 利用全局函数打印类中的公共属性
// cout 是类ostream(输出流)的对象,且全局只能有一个,用引用的方式传递
//全局函数重载,更能够将两个对象按顺序用重载运算符连接起来
class Person
{
public:
int m_A;
int m_B;
};
// void operator<<(cout, Person &p) //报错. cout 是类ostream(输出流)的对象,对象要用引用传递. 下行为正确代码
ostream &operator<<(ostream &cout, Person &p)
{
cout << "m_A = " << p.m_A << " m_B = " << p.m_B << endl; //将'<<' 运算符重载为 执行该函数体(打印a属性和打印b属性)
return cout; // 解引用ostream,返回cout,可以无限往后追加输入. cout << p << "hello "<< endl<<...<<...<<.. ,需注意<<左右两边连接的是对象
}
void test01()
{
// 方式1
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p.m_A << endl;
cout << p.m_B << endl;
cout << p; //能够成功输出"m_A = 10 p.m_B = 10"
cout << p << endl; // 该行能够成功运行的前提,是cout << p 返回的是一个对象,这个新的对象又重新调用了endl
}
int main()
{
test01();
system("pause");
return 0;
}
// 14.2.2.2 利用全局函数打印类中的私有属性(引入友元)
class Person
{
friend ostream &operator<<(ostream &cout, Person &p); //被访问类中加入友元函数声明
private:
int m_A; //本例中是私有属性了
int m_B;
};
// void operator<<(cout, Person &p) //报错. cout 是类ostream(输出流)的对象,对象要用引用传递. 下行为正确代码
ostream &operator<<(ostream &cout, Person &p)
{
cout << "m_A = " << p.m_A << " m_B = " << p.m_B << endl; //将'<<' 运算符重载为 执行该函数体(打印a属性和打印b属性)
return cout; // 解引用ostream,返回cout,可以无限往后追加输入. cout << p << "hello "<< endl<<...<<...<<.. ,需注意<<左右两边连接的是变量
}
void test01()
{
// 方式1, 会报错:
// Person p;
// p.m_A = 10; //会报错. 因为p.m_A是私有属性,p.m_A = 10不在友元函数里, 没法赋初值.
// p.m_B = 10;
// cout << p.m_A << endl;
// cout << p.m_B << endl;
// cout << p << "hello worle"<,<,==重载
// 重载关系运算符(>,<,==,!=),可以让两个自定义类型对象进行对比操作.
// ==可以判断数字1和2是否相等.能否让==来判断对象p1和对象p2是否相等?:
// Person p1;
// Person p2;
// if(p1==p2){
// cout<<"p1和p2相等"<m_Name == p.m_Name && this->m_Age == p.m_Age) //自身的名字==传入p的名字 并且 自身的年龄==传入p的 年龄
{
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;
}
Person c("孙悟空", 18);
Person d("孙悟空", 20);
if (c != d)
{
cout << "c 和 d 不相等" << endl;
}
else
{
cout << "c 和 d 相等" << endl;
}
}
int main()
{
test01();
system("pause");
return 0;
}
// 14.6 函数调用运算符 () 重载(仿函数)
// 由于重载后使用的方式非常像函数的调用,因此称为仿函数
// 仿函数没有固定写法,非常灵活
//语法:void operator()(参数列表){ 函数体} //有两个()()
// 14.6.1 仿函数和普通函数
// 仿函数使用起来和函数十分相似.以下是区分:
// 1.仿函数定义:class 类名 { void operator()(参数) { } }
// 函数定义:void 函数名(参数) { }
// 2.仿函数调用:实例对象(参数);
// 函数调用: 函数名(参数)
class MyPrint // 定义一个打印类
{
public:
void operator()(string text) // 用于重载()操作符的仿函数. string text是参数
{
cout << text << endl;
}
}; //类内
//函数和仿函数定义的方式不同
void MyPrint02(string test) //写另一个打印函数. 这个函数的功能就是打印, 和仿函数做区分, 二者调用的方式很像,但定义的方式不同
{
cout << test << endl;
}
void test01()
{
MyPrint myFunc;
myFunc("hello world"); //重载函数调用. // 由于使用起来非常类似于函数调用,因此称为仿函数
MyPrint02("hello world"); // 普通函数调用. 函数和仿函数调用的方式不同. 二者结果一样
}
int main()
{
test01();
system("pause");
return 0;
}
// 14.6.2 仿函数非常灵活;仿函数结合匿名对象
//仿函数非常灵活,没有固定的手法. 可以传一个参数,也可以传两个参数. 可以返回&,也可以返回值int. int operator()(int v1, int v2)
//仿函数结合匿名对象: 先显式法MyAdd()创建匿名对象,然后第二个括号是仿函数.
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; // 20
//仿函数结合匿名对象
//先显式法MyAdd()创建匿名对象,然后第二个括号是仿函数
cout << "MyAdd()(10,10) = " << MyAdd()(10, 10) << endl; // 20
}
int main()
{
test02();
system("pause");
return 0;
}
//第十五章 继承
// 15.1 继承
//继承是面向对象三大特性之一. 继承的好处:可以减少重复的代码
// 语法:class 子类: 继承方式 父类
// class A : public B;
// A 类称为 子类 或 派生类
// B 类称为 父类 或 基类
// 派生类中的成员,包含两大部分:
// 一类是从基类继承过来的,一类是自己增加的成员
// 从基类继承过过来的表现其共性,而新增的成员体现了其个性
class BasePage //公共页面类(父类)
{
public:
void header()
{
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
};
class CPP : public BasePage // C++页面类(子类一)
{
public:
void content()
{
cout << "C++ 学科视频" << endl;
}
};
class Python : public BasePage // Python页面类(子类二)
{
public:
void content()
{
cout << "Python 学科视频" << endl;
}
};
int main()
{
// C++页面
cout << "C++ 下载视频页面如下: " << endl;
CPP cp;
cp.header();
cp.footer();
cp.content();
// Python页面
cout << "Python 下载视频页面如下: " << endl;
Python py;
py.header();
py.footer();
py.content();
system("pause");
return 0;
}
// 15.2 继承方式
// 继承方式一共有三种:
// 公共继承, class Son1 :public Base1 {...} ; 按原权限继承
// 保护继承, class Son2 :protected Base1 {...}; 父类中比protected更高的权限都变为子类中的protected权限
// 私有继承, class Son3 :private Base1 {...} ; 父类中比private更高的权限都变为子类中的private权限
//含义:
// 父类中可能会有公共内容(public), 保护内容(protected), 私有内容(private)
// 公共成员:类内和类外都可以访问. 子类怎么继承,就变成什么成员
// 保护成员:类内可以访问.类外不可以访问.
// 对于父类中私有成员,无论哪种继承方式,子类都无法访问.
// 子类|公共继承|保护继承|私有继承|
// |父类 |
// |公共成员|公共成员|保护成员|私有成员|
// |保护成员|保护成员|保护成员|私有成员|
// |私有成员|不可访问|不可访问|不可访问|
//先定义一个父类,再分几种情况分析
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
// 15.2.1 公共继承
class Son1 : public Base1
{
public:
void func()
{
m_A = 10; // 可访问 public权限,父类中的公共权限成员,到子类中依然是公共权限
m_B = 10; // 可访问 protected权限,父类中的保护权限成员,到子类中依然是保护权限
// m_C = 10; // 不可访问,父类中的私有权限成员,子类访问不到
}
}; //类内
void test01()
{
Son1 s1;
s1.m_A = 100; //其他类只能访问到公共权限
// s1.m_B = 100; //报错. 到了son1中m_B是保护权限,类外访问不到
}
// 15.2.2 保护继承
class Son2 : protected Base1
{
public:
void func()
{
m_A = 100; // 可访问 protected权限,父类中的公共成员,到子类中变为保护权限
m_B = 100; // 可访问 protected权限,父类中保护权限成员,到子类中变为保护权限
// m_C = 100; // 父类中的私有成员子类不可访问
}
}; //类内
void test02()
{
Son2 s;
// s.m_A = 100;//在Son2中m_A变为保护权限,因此类外不可访问
// s.m_B = 100;//在Son2中m_B变为保护权限,因此类外不可访问
}
// 15.2.3 私有继承
class Son3 : private Base1
{
public:
void func()
{
m_A = 100; // 可访问 private权限,父类中公共成员,在子类中变为 私有成员
m_B = 100; // 可访问 private权限,父类中保护成员,在子类中变为 私有成员
// m_C; // 父类中私有成员子类不可访问
}
}; //类内
void test03()
{
Son2 s;
// s.m_A = 100;// 在Son3中m_A变为私有权限,因此类外不可访问
// s.m_B = 100;// 在Son3中m_B变为私有权限,因此类外不可访问 //结果和保护继承一样
}
// 15.2.3.1 孙子继承儿子
class GrandSon3 : public Son3
{
public:
void func()
{
// Son3的私有属性在GrandSon3中都无法访问到
// m_A;// 都报错
// m_B;
// m_C;
}
};
int main()
{
system("pause");
return 0;
}
// 15.3 继承中的对象模型
// 问题:从父类继承过来的成员,哪些属于子类对象中?
// 结论
// 父类中所有非静态成员属性都会被子类继承下去
// 父类中私有成员属性子类只是访问不到,被隐藏了,但是确实被继承下去
// 拓展:利用开发人员命令提示工具查看对象模型
// 跳到文件所在盘 D:
// 跳转到文件路径 cd 具体路径下
// 查看单个类布局 cl /d1 reportSingleClassLayouts类名 文件名
//可以看到Son类中有哪些属性
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C; // 私有成员只是被隐藏了,但是还是会继承下去. 只是子类没法访问而已.
};
// 公共继承
class Son : public Base
{
public:
int m_D; //子类的自身属性D
};
void test01()
{
cout << "sizeof Son = " << sizeof(Son) << endl; // 16
// 父类中所有非静态成员属性都会被子类继承下去
// 父类中私有成员属性子类只是访问不到,被隐藏了,但是确实被继承下去
}
int main()
{
test01();
system("pause");
return 0;
}
// 15.4 继承中构造与析构顺序
//问题:子类继承父类后,当创建子类对象,也会调用父类的构造函数,父类和子类的构造和析构顺序是谁先谁后?
//总结:继承中 先调用父类构造函数,再调用子类构造函数.
//析构顺序与构造相反,先析构子类,再析构父类。
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;
// Base 构造函数!
// Son 构造函数!
// Son 析构函数!
// Base 析构函数!
}
int main()
{
test01();
system("pause");
return 0;
}
// 15.5 继承同名成员处理方法
// 问题:当子类与父类出现同名的成员(成员函数或成员属性),如何通过子类对象,访问到子类或父类中同名的数据呢?
// 通过子类对象,访问子类同名成员 直接访问即可 s.m_A s.func();
// 通过子类对象,访问父类中间同名成员,会优先调用的是子类中的同名成员,除非加作用域 s.Base::m_A s.Base::func();
// 父类中重载的成员函数由于函数名也与子类相同,一起被屏蔽了. 除非加作用域 s.Base::func(100);
class Base
{
public:
Base()
{
m_A = 100;
}
void func() // 成员函数
{
cout << "Base - func() 调用" << endl;
}
void func(int a) // 成员函数 的重载函数
{
cout << "Base - func(int a) 调用" << endl;
}
int m_A;
}; //类内
class Son : public Base
{
public:
Son()
{
m_A = 200;
}
void func() // 子类中有同名成员函数
{
cout << "Son - func() 调用" << endl;
}
int m_A; //子类中有同名属性
}; //类内
// 15.5.1 同名成员属性处理方式: 如果通过子类对象,访问到父类中间同名成员,需要加作用域
void test01()
{
Son s;
cout << "Son 下 m_A: " << s.m_A << endl;
cout << "Base 下 m_A: " << s.Base::m_A << endl; // 通过子类对象,访问到父类中间同名成员
}
// 15.5.2 同名成员函数处理方式
void test02()
{
Son s;
s.func(); // 直接调用的是子类中的同名成员
s.Base::func(); // 加作用域调用到父类中同名成员函数
//当子类出现和父类同名的成员函数时,会优先调用子类成员.同时会屏蔽父类中所有的同名成员函数,不管是有参的还是无参的.因此该重载的成员函数由于函数名与子类相同,一起被屏蔽了
s.Base::func(100); // 如果想访问父类中被隐藏的同名重载成员,需要加作用域
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
// 15.6 继承同名静态成员处理方式
// 静态成员的特点: 1.编译阶段分配内存。2.所有对象共享同一份数据。3,类内声明 类外初始化
// 静态成员可以通过对象访问 或 通过类名访问
// 总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名
// 15.6.1 同名静态成员属性:
// 通过对象访问 s.m_A; s.Base::m_A; (子类直接访问; 子类通过父类访问;)
// 通过类名访问 Son::m_A; Son::Base::m_A;(第一个::代表通过类名方式访问 第二个::代表访问父类的作用域下)
// 15.6.2 同名静态成员函数:
// 通过对象访问 s.func(); s.Base::func();
// 通过类名访问 Son::func();; Son::Base::func();
// 子类出现和父类同名的静态成员函数,也会隐藏父类中所有的同名成员函数.如果想访问父类中被隐藏同名成员,需要加作用域
class Base
{
public:
static int m_A;
static void func() // 成员函数
{
cout << "Base - static void func()" << endl;
}
static void func(int a) // 成员函数 的重载函数
{
cout << "Base - static void func(int a)" << endl;
}
}; //类内
int Base::m_A = 100; //静态成员, 类内声明 类外初始化
class Son : public Base
{
public:
static int m_A; // 需要赋初值
static void func() // 子类中有同名成员函数
{
cout << "Son - static void func()" << endl;
}
}; //类内
int Son::m_A = 200; //子类中有同名属性
// 15.6.1同名静态成员属性
void test01()
{
// 1. 通过对象来访问
cout << "通过对象访问:" << endl;
Son s;
cout << "Son 下 m_A = " << s.m_A << endl; // 200
cout << "Base 下 m_A = " << s.Base::m_A << endl; // 100
cout << endl;
// 2. 通过类名访问
cout << "通过类名访问:" << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
cout << "Base 下 m_A = " << Son::Base::m_A << endl; // 第一个::代表通过类名方式访问 第二个::代表访问父类的作用域下
cout << endl;
}
// 15.6.2同名静态成员函数
void test02()
{
// 1.通过对象访问
cout << "通过对象访问:" << endl;
Son s;
s.func();
s.Base::func();
cout << endl;
// 2.通过类名访问
cout << "通过类名访问:" << endl;
Son::func();
Son::Base::func();
// Son::func(100); //报错
// 2.1 直接调用父类中成员函数的(重载),会报错.因为同名的函数也被屏蔽了
//当子类出现和父类同名的静态成员函数时,会优先调用子类成员.同时会屏蔽父类中所有的同名成员函数,不管是有参的还是无参的.因此该重载的成员函数由于函数名与子类相同,和上面第一个的函数一起被屏蔽了
Son::Base::func(100); //成功调用父类中成员函数的重载函数 //如果想访问父类中被隐藏同名成员,需要加作用域
cout << endl;
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
// 15.7 多继承语法(不建议用)
// C++允许一个类继承多个类. 一个孩子可以继承多个父亲,吕布语法
// 语法: class 子类 :继承方式 父类1 , 继承方式 父类2...
//如 class 吕布 : public 丁原,public 董卓 , public 王允
// 多继承可能会引发父类中有同名成员出现,需要加作用域区分
// C++实际开发中不建议用多继承
// 多继承语法(不建议使用)
class Base1
{
public:
Base1() // 无参构造函数
{
m_A = 100;
}
int m_A;
}; //父类1
class Base2
{
public:
Base2() // 无参构造函数
{
m_A = 200;
}
int m_A;
}; //父类2
// 子类 需要继承Base1和Base2
class Son : public Base1, public Base2
{
public:
Son()
{
m_C = 300;
m_D = 400;
}
int m_C;
int m_D;
};
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl; // 16,包括继承父类的两个
// 当父类中出现了重名的成员,需要加作用域区分. 如本例中,Base1和Base2都有m_A成员属性
cout << "Base1::m_A = " << s.Base1::m_A << endl; // 100
cout << "Base2::m_A = " << s.Base2::m_A << endl; // 200
}
int main()
{
test01();
system("pause");
return 0;
}
// 15.8 菱形继承
// 菱形继承概念:
// 两个派生类继承同一个基类
// 又有某个类同时继承者两个派生类
// 这种继承被称为菱形继承,或者钻石继承
// 菱形继承问题:
// 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性. 这个根据我们前面所学,加入作用域即可区分.此时两份数据都保存了
// 草泥马继承了羊的m_Age,也继承了驼的m_Age.其实我们应该清楚,这份数据我们只需要一份就可以. 虚继承:是谁最后修改谁为准, 只保存一份数据
// 解决方法:
// 利用虚继承可以解决菱形继承的问题
// 在继承之前加上关键字 virtual 变为虚继承
// Animal类 称为 虚基类
// 通过虚继承可以保存最后的数据: 底层实现原理:
//通过终端cd进入该程序路径, 然后输入cl /d1 reportSingleClassLayoutSheepTuo查询类结构,可看到在类下面,子类继承的并非数据,而是vbptr 虚基类指针
// vbptr 虚基类指针: v-virtual ,b-base, ptr - pointer
// 指针中存储一个地址的偏移量,加上就可以指向一个新地址.不同的子类,自父类中继承的虚基类指针,最后都指向同一个地址,因此虚基类最后只会保留一个数据
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 = 18;
st.Tuo::m_Age = 28;
// 当出现菱形继承的时候,两个父类拥有相同数据,通过虚继承保存最后的数据,即以28为准
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; // 28
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; // 28
}
int main()
{
system("pause");
return 0;
}
//第十六章 多态
//多态: 让子类重写父类中的虚函数.
// 多态是C++面向对象三大特性之一
// 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名. 上面我们学到的都是静态多态.
// 动态多态: 派生类和虚函数实现运行时多态. 一般我们说的多态都是动态多态,本章介绍的也都是动态多态.
// 静态多态和动态多态区别:静早动晚
// 静态多态的函数 地址早绑定 - 编译阶段确定函数地址
// 动态多态的函数 地址晚绑定 - 运行阶段确定函数地址. 执行让猫,狗说话
// // 16.1 多态基本语法
// 动态多态满足条件:
// 有继承关系
// 子类重写父类中的虚函数.重写: 函数名相同+形参列表中东西相同+函数返回值类型相同(重载不要求返回值相同)。
// 重写时父类的 virtual 必写,子类的 virtual 可写可不写
// 用父类指针或引用 接收子类对象 void dospeak(Animal& animal); dospeak(cat);
class Animal // 动物类
{
// public:
// void speak() // 原父类成员函数
// {
// cout << "动物在说话" << endl;
// }
public:
virtual void speak() // 改为 虚函数 virtual
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal // 猫类
{
public:
void speak() //子类需重写父类中的虚函数; 重写时父类的 virtual 必写,子类的 virtual 可写可不写
{
cout << "小猫在说话" << endl;
}
};
//狗类
class Dog : public Animal
{
public:
void speak() //重写
{
cout << "小狗在说话" << endl;
}
};
void dospeak(Animal & animal)
//动态多态中父类引用可接收 子类的传递对象,允许父子间类型转换,无需强制转换 dospeak(cat);
//假设你有多个类同时继承一个类,而这个几个类中(包括父类)都有同名方法那么参数直接写父类类名就可以
{
animal.speak(); // 加了virtual后,由于加入的对象不同,确定不同函数地址
}
void test01()
{
Cat cat;
dospeak(cat); // Animal &animal=cat //动物在说话(如果父类前不加入virtual)
//本意是想让猫来说话->将父类变为虚函数
Dog dog;
dospeak(dog); //动物在说话(如果父类前不加入virtual)
//本意是想让狗来说话->将父类变为虚函数
// 地址早绑定-动物说话, 地址晚绑定-猫说话,狗说话
//早绑定:animal类里面的说话函数编译完后已经在内存里的某个地方存在了,不管传入参数是哪个子类,都只执行父类中的函数
//晚绑定:将 “父类”变成虚职(父类前加入virtual关键字),函数也就不访问它,而访问子类了,
}
void test02()
{
cout << "sizeof Animal = " << sizeof(Animal) << endl;
//父类变为虚函数前,sizeof(Animal) = 1 .空类
//父类变为虚函数后,sizeof(Animal) = 4 .原因:变虚函数后,存储的东西变成了指针,无论什么类型的指针,都占4个字节.
//这种现象会引出下一节的内容: 多态原理剖析
}
int main()
{
test01();
test02();
system("pause");
return 0;
}
// 16.2 多态原理剖析
//父类变为虚函数后,sizeof(Animal) =4 .原因:变虚函数后,存储的东西变成了指针(vfptr),无论什么类型的指针,都占4个字节.
// vfptr虚函数(表)指针,virtual function pointer ,指针会指向一个虚函数表vftable.
// vftable虚函数表,virtual function table 里面存放虚函数表.
// vftable虚函数表内部记录一个虚函数的地址.&Animal::speak()
// vfptr, 区分vbptr虚基类指针
// Animal类内结构:vfptr -> vftable: &Animal::speak()
// 子类Cat类内结构:子类继承父类中所有的内容, vfptr -> vftable: &Animal::speak()
// 但当子类重写父类虚函数,子类中的虚函数表内部,会替换成子类的虚函数地址.vfptr -> vftable: &Cat::speak()
// 当父类的指针或引用指向子类对象时,发生多态. Animal &animal=cat ,会从cat的虚函数表中找Cat::speak() 并执行猫在叫
// 多态本质上就是用virtual关键字在底层增加了一层指针,根据传入不同的子类对象,
// 来指向对应的子类虚函数表.从cat的虚函数表中调用相关的虚函数.
// 16.3 多态案例1 —— 计算机类
// 案例描述:分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
// 多态的优点:
// 代码组织结构清晰
// 可读性强
// 利于前期和后期的扩展以及维护
//总结:C++开发提倡利用多态设计程序架构,因为多态优点很多
// 普通写法
// 如果想扩展新的功能,需要修改源码
// 在真实开发中,提倡 开闭原则
// 开闭原则:对扩展进行开放,对修改进行关闭
// 普通写法
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;
}
// 如果想扩展新的功能,需要修改源码.如想要增加一个指数运算的功能,要修改源码
// 在真实开发中,提倡 开闭原则.
// 开闭原则:对扩展进行开放,对修改进行关闭.不让修改,只让扩展.增强原有的功能,而不去修改源代码.
// 可以学习《设计模式》充分理解开闭原则,单一职责,里氏替换,迪米特法则,依赖倒置 5个基础原则,后续的设计模式都是基于这五个基础原则的演化。
}
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 // 计算器的抽象类.
// 里面只有虚函数的类,叫抽象类. 抽象类中什么功能都不写,只有一个getResult()虚函数和运算用的变量.
{
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()
{
// 加法运算
// 创建一个加法计算器的对象. 用父类的指针指向子类的对象,此时发生多态
//* abc 是父类的指针, new AddCalculator是创建了一个子类的对象
AbstractCalculator *abc = new AddCalculator; // 用父类的指针指向子类的对象,此时发生多态
abc->m_Num1 = 100;
abc->m_Num2 = 100;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; //用完后手动释放堆区数据. 堆区数据除非自己释放,否则堆区只会在整个程序运行完后释放
//减法运算
abc = new SubCalculator;
abc->m_Num1 = 100;
abc->m_Num2 = 100;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
//乘法运算
abc = new MulCalculator;
abc->m_Num1 = 100;
abc->m_Num2 = 100;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}
int main()
{
test01();
cout << endl;
test02();
system("pause");
return 0;
}
// 16.4 纯虚函数和抽象类
// 在多态中,通常父类中虚函数是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
// 纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ; //virtual void func() = 0;
// 抽象类: 只要类中有一个纯虚函数,这个类称为抽象类. 抽象类=接口
// 抽象类特点:
// 1.无法实例化对象 (像一个无法下蛋的母鸡)
// 2.抽象类的子类 必须重写父类中的纯虚函数,否则这个子类也无法实例化对象 Son s; 因为这个子类也属于抽象类
class Base
{
public:
virtual void func() = 0; // 纯虚函数
};
class Son : public Base // 继承
{
public:
virtual void func() // 重写父类中的纯虚函数
{
cout << "func 函数调用" << endl;
};
};
void test01()
{
// Base b;// 抽象类Base是无法实例化对象,在栈区
// new Base;// 抽象类是无法实例化对象,在堆区也不行.
//一句话记住了,只要够虚,就没有对象,大师我悟了
Son s; //检验:不报错. 子类重写父类中的纯虚函数后(子类加virtual),才能实例化对象
//调用多态,用父类指针接收子类对象
Base *base = new Son; // new 什么对象,就调用什么对象 func 函数
base->func(); // 调用子类 func 函数
}
int main()
{
test01();
system("pause");
return 0;
}
// 16.5 多态案例2 —— 制作饮品
// 案例描述:
// 制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
// 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
class AbstractDrinking //基类
{
public:
// 基类里面都写成纯虚函数,不做任何处理
// 煮水
virtual void Boil() = 0;
// 冲泡
virtual void Brew() = 0;
// 倒入杯中
virtual void PourInCup() = 0;
// 加入辅料
virtual void PutSomething() = 0;
// 制作饮品
void makeDrink() // 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 * abs) // 父类的指针
{
abs->makeDrink();
// 制作完delete
delete abs; // 堆区数据手动开辟,手动释放
}
void test01()
{
// 制作咖啡
doWork(new Coffee); // AbstractDrinking *abs = new Coffee
cout << " --------------- " << endl;
// 制作茶叶
doWork(new Tea); // AbstractDrinking *abs = new Tea
//不同的子类,可以通过多态共用父类中makeDrink()的接口,一个接口有多种形态,同一个接口可以制作不同的饮品.
}
int main()
{
test01();
system("pause");
return 0;
}
// 16.6 虚析构和纯虚析构
// 使用场景:多态使用时,父类指针无法释放子类对象.
// 如果子类中有属性开辟到堆区,那么父类指针在释放时只会调用父类自己的析构函数,而无法调用到子类的析构代码,此时堆内存中的子类数据就无法被清除,造成内存泄露
// 解决方式:将父类中的析构函数改为虚析构或者纯虚析构
// 本质和虚函数是一样的,在父类调用自己的析构函数时通过vfptr调用实际子类的析构函数
// 虚析构和纯虚析构共性:
// 都可以解决父类指针释放子类对象,如果父类没有写成虚析构或者纯虚析构,就不会走子类的虚构代码。
// 都需要有具体的函数实现,就是虚析构、纯虚析构要进行函数实现。
// 虚析构和纯虚析构区别:
// 如果是纯虚析构,该类属于抽象类,无法实例化对象
// 纯虚析构, 与虚析构只能有一个存在, 需要声明也需要实现
// 纯虚函数,与纯虚析构不同,只要声明不需要实现
// 虚析构语法:virtual ~类名(){}
// 纯虚析构语法:类内声明 —— virtual ~类名() = 0;
// 类外实现 —— 类名::~类名(){}
// 总结:
// 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
// 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。有指针情况就会有堆区
// 拥有纯虚析构函数的类也属于抽象类
// 纯虚函数和纯虚析构的区别
// 纯虚析构, 与虚析构只能有一个存在, 需要声明也需要实现
// 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
// 虚析构和纯虚析构 —— 解决子类中的代码调用不到的问题,存在指针类型,在堆区
// 16.6.1 父类指针释放子类对象不干净的问题
class Animal
{
public:
Animal() // 无参构造函数调用
{
cout << "Animal 构造函数调用" << endl;
}
~Animal() // 析构函数
{
cout << "Animal 析构函数调用" << endl;
}
// 纯虚函数 speak() .纯虚函数,与后面学的纯虚析构不同,纯虚函数只要声明不需要实现
virtual void speak() = 0;
} //类内
class Cat : public Animal
{
public:
Cat(string name) // 有参构造函数
{
cout << "Cat 构造函数调用" << endl;
m_Name = new string(name); // new 一个string,返回的是string指针,m_Name是一个指针
}
virtual void speak() // 子类重写父类纯虚函数,多态的条件
{
cout << *m_Name << " 小猫在说话" << endl;
}
public:
string *m_Name; // 让小猫的名称创建在堆区
//注意一下,这里老师是为了推进下面的课程才这么写的,属性不是必要开辟在堆区
//加入Cat析构函数. 使得当函数执行完,自动释放存放在堆区的数据m_Name,
~Cat()
{
if (m_Name != NULL)
{
cout << "Cat析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
// delete是清空指针区域的内容,并不清空指针(m_Name),NULL是把指针替换为NULL地址
}
}
}; //类内
void test01()
{
Animal *animal = new Cat("Tom"); // new一个对象 会自动调用构造函数. //Tom小猫在说话
animal->speak();
delete animal; //使用delete释放animal这个指针的时候,会自动调用animal析构函数。
// Animal 构造函数调用
// Cat 构造函数调用
// Tom小猫在说话
// Animal 析构函数调用
//发现没走"Cat 析构函数调用",则没走delete m_Name;,堆区数据m_Name没有被释放干净. 本来应该有两个指针要释放,现在释放了animal,没有释放m_Name
//原因: 父类指针在析构时,不会调用子类中析构函数,导致子类如果有堆区的属性,出现内存的泄露情况
//内存泄漏(Memory Leak): 是指程序中已动态分配的[堆内存]由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
//=斩草不除根
//利用虚析构可以解决父类指针释放子类对象不干净的问题
}
int main()
{
test01();
system("pause");
return 0;
}
// 16.6.2 16.6.3 利用虚析构 或 纯虚析构可以解决父类指针释放子类对象不干净的问题
// 优化思路: 要通过履行"Cat 析构函数调用",将cat类中创建的堆区数据m_Name释放干净.
// 方式:将Animal类中的"析构"改为"虚析构/纯虚析构"可以解决父类指针释放子类对象不干净的问题. virtual 可以强制调看到子类.
class Animal
{
public:
Animal() // 无参构造函数调用
{
cout << "Animal 构造函数调用" << endl;
}
virtual void speak() = 0; // 纯虚函数speak(),与纯虚析构不同,纯虚函数只要声明不需要实现
// 16.6.2 虚析构 可以解决 父类指针释放子类对象时不干净的问题
virtual ~Animal() // 析构函数 变为 虚析构,这样Cat析构函数就会调用出来
{
cout << "Animal 虚析构函数调用" << endl;
}
// 16.6.3 纯虚析构, 与虚析构只能有一个存在, 需要声明也需要实现(实现见类外).函数实现通常写在"{ }"里面
virtual ~Animal() = 0;
}; //类内
// 纯虚析构的实现.纯虚析构必须要实现,而且实现必须写在类外面.
Animal::~Animal() // Animal作用域 下的纯虚析构函数~Animal()
{
cout << "Animal 纯虚析构函数调用" << endl;
}
class Cat : public Animal
{
public:
Cat(string name) // 有参构造函数
{
cout << "Cat 构造函数调用" << endl;
m_Name = new string(name); // new 一个string,返回的是string指针,m_Name是一个指针
}
virtual void speak() // 子类重写父类
{
cout << *m_Name << " 小猫在说话" << endl; // Tom小猫在说话
}
~Cat() // 析构函数,用于释放堆区数据
{
if (m_Name != NULL)
{
cout << "Cat 析构函数调用" << endl;
delete m_Name;
m_Name == NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom"); // new一个对象 会自动调用构造函数
animal->speak();
delete animal; //使用delete释放animal这个父类指针的时候,会自动调用父类析构函数。
// Animal 构造函数调用
// Cat 构造函数调用
// Tom小猫在说话
// Cat 析构函数调用
// Animal 析构函数调用
}
int main()
{
test01();
system("pause");
return 0;
}
// 16.7 多态案例3 —— 电脑组装
// 16.7.1 抽象每个零件的类.CPU, 显卡, 内存条三个抽象父类.
// 16.7.2 电脑类.电脑的类中有三个指针维护三个零件:CPU, 显卡, 内存条
// 16.7.3 具体零件厂商:Inter厂商, Lenovo厂商.
// 16.7.4 创建电脑
// 16.7.1 抽象出每个零件的类
// 1.抽象CPU类
class CPU // 抽象类
{
public:
// 抽象计算函数
virtual void calculate() = 0;
};
// 2.抽象显卡类
class VideoCard // 抽象类
{
public:
// 抽象显示函数
virtual void display() = 0;
};
// 3.抽象内存条类
class Memory // 抽象类
{
public:
// 抽象存储函数
virtual void storage() = 0;
};
// 16.7.2 电脑类
class Computer
{
// 构造函数中传入三个零件指针
public:
Computer(CPU *cpu, VideoCard *vc, Memory *mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}
// 提供工作函数
void work()
{
// 让零件工作起来,调用接口
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
// 提供析构函数 释放3个电脑零件
~Computer()
{
// 释放CPU零件
if (m_cpu != NULL)
{
delete m_cpu;
m_cpu = NULL;
}
// 释放显卡零件
if (m_vc != NULL)
{
delete m_vc;
m_vc = NULL;
}
// 释放内存条零件
if (m_mem != NULL)
{
delete m_mem;
m_mem = NULL;
}
}
private:
// 电脑的类中有三个指针维护三个零件
CPU *m_cpu; // CPU的零件指针
VideoCard *m_vc; // 显卡的零件指针
Memory *m_mem; // 内存条零件指针
};
// 16.7.3 具体零件厂商
// Inter厂商
class InterCpu : public CPU
{
public:
virtual void calculate()
{
cout << "Inter CPU开始计算" << endl;
}
};
class InterVideoCard : public VideoCard
{
public:
virtual void display()
{
cout << "Inter 显卡开始显示" << endl;
}
};
class InterMemory : public Memory
{
public:
virtual void storage()
{
cout << "Inter 内存条开始存储" << endl;
}
};
// 联想Lenovo厂商
class LenovoCpu : public CPU
{
public:
virtual void calculate()
{
cout << "Lenovo CPU开始计算" << endl;
}
};
class LenovoVideoCard : public VideoCard
{
public:
virtual void display()
{
cout << "Lenovo 显卡开始显示" << endl;
}
};
class LenovoMemory : public Memory
{
public:
virtual void storage()
{
cout << "Lenovo 内存条开始存储" << endl;
}
};
// 16.7.4 组装不同的电脑
void test01()
{
//第一台电脑组装,CPU为父类,InterCpu是子类
CPU *intelCpu = new InterCpu; // 多态技术,用父类的指针指向子类对象
VideoCard *intelCard = new InterVideoCard;
Memory *intelMem = new InterMemory;
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 InterVideoCard, new LenovoMemory);
;
computer3->work();
delete computer3;
}
int main()
{
test01();
system("pause");
return 0;
}