本文章建立在已学C语言的基础上
第一阶段
生成随机数函数:rand()。rand()%100指的是生成0~99的随机数。这样生成的随机数每次都是一样顺序出现的,为了防止这个问题出现,我们可以使用随机数种子,如下代码
#include<iostream>
#include<ctime>
using namespace std;
int main()
{srand((unsigned int)time(NULL));int num = rand() % 100;
}
一,C++初识
1,输入输出函数(cin cout)
在 C++ 中,iostream
是输入 / 输出流(Input/Output Stream)库的一部分。它提供了用于处理输入和输出操作的功能,是 C++ 标准库的重要组成部分。这个库中的类和对象允许程序员以一种方便、灵活且类型安全的方式来进行数据的输入和输出,而不需要直接操作底层的输入 / 输出系统
除了标准输入 / 输出,iostream
的概念还可以扩展到文件输入 / 输出。C++ 通过fstream
头文件(它也基于iostream
的基础概念)来实现文件的读写操作。可以使用ifstream
类进行文件读取,ofstream
类进行文件写入。这个以后再介绍
这里着重讲一下输入和输出函数(对应C语言的printf 和 scanf)
iostream
中的cin
对象用于从标准输入设备(通常是键盘)读取数据。使用实例如下
#include <iostream>
int main() {int num;std::cout << "请输入一个整数: ";std::cin >> num;std::cout << "你输入的整数是: " << num << std::endl;return 0;
}
std::cin >> num;
语句会暂停程序的执行,等待用户从键盘输入一个整数。>>
运算符在这里被重载,用于将输入的数据提取并存储到num
变量中。它可以读取多种基本数据类型,如int
、double
、char
等,还可以读取字符串(使用std::string
类型配合getline
函数或者>>
操作符)。
iostream
中的cout
对象用于将数据输出到标准输出设备(通常是显示器)
#include <iostream>
int main() {int a = 10;std::cout << "a的值是: " << a << std::endl;return 0;
}
std::cout << "a的值是: " << a << std::endl;
语句将字符串"a的值是: "
和变量a
的值输出到屏幕上。<<
运算符也是被重载的,它允许将多个数据依次输出。endl
是一个操纵符(manipulator),它的作用是输出一个换行符并刷新输出缓冲区,这样可以确保输出的内容及时显示在屏幕上。
上述的std::表示cin
和cout
是std
命名空间下的对象。std
是标准库使用的命名空间。命名空间主要是为了避免名称冲突。C++ 标准库包含了大量的类、函数、对象等,如果没有命名空间,当用户定义的名称和标准库中的名称相同时,就会产生混淆。用std::表示cin和cout来自此库。如果使用using指令(using namespace std),则可以不加std::。如下代码:
#include <iostream>
using namespace std;
int main()
{cout << "Hello World" << endl;//在屏幕中输出Hello Worldsystem("pause");return 0;}
不过这种方式在大型项目中不推荐,因为它会将std
命名空间中的所有名称引入到当前的作用域,可能会导致命名冲突。在这个代码中,using namespace std;
使得cout
、cin
和endl
可以直接使用,而不需要加上std::
前缀。但是,如果在程序中还定义了其他与std
命名空间中同名的变量或函数,就会出现问题。
更安全的做法是使用using
声明,它只引入特定的名称到当前作用域。例如以下代码,就不会出现命名冲突的问题
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main() {int num;cout << "请输入一个整数: ";cin >> num;cout << "你输入的整数是: " << num << endl;return 0;
}
这里通过using std::cout;
、using std::cin;
和using std::endl;
分别将cout
、cin
和endl
引入到当前作用域,这样就可以在不使用std::
前缀的情况下使用它们,同时又减少了命名冲突的风险。因为只有明确声明的这些名称才会被引入,而不是整个std
命名空间。
2,using指令
在 C++ 中,using
指令是一种用于管理命名空间(namespace)的机制。命名空间用于将一组相关的名称(如类、函数、对象等)组织在一起,以避免名称冲突。using
指令允许程序员在一定程度上简化对命名空间中名称的访问。
using namespace
形式
using namespace
语句用于将整个命名空间的内容引入到当前的作用域。例如,using namespace std;
是在 C++ 程序中常见的一种用法,这里的std
是 C++ 标准库的命名空间。
当使用这条指令后,就可以直接使用std
命名空间中的所有名称(如std::cout
、std::cin
等),而不需要在每个名称前都加上std::
前缀。
潜在问题
名称冲突:当使用using namespace std;
这样的指令将整个命名空间引入时,如果程序中定义的名称与命名空间中的名称相同,就会产生冲突。例如,如果在程序中定义了一个名为cout
的变量,在引入std
命名空间后,编译器就无法确定cout
是指自定义的变量还是std
命名空间中的cout
对象。
可读性降低:在大型项目中,很难确定一个未加限定的名称(如cout
)是来自哪个命名空间。这会使得代码的维护和理解变得困难。
using
声明形式
除了using namespace
这种形式,还有using
声明。using
声明用于将命名空间中的特定名称引入到当前作用域。例如,using std::cout;
只将std
命名空间中的cout
对象引入到当前作用域。这样就可以在不使用std::
前缀的情况下使用cout
,同时又避免了引入整个命名空间带来的问题。
这种方式在一定程度上兼顾了方便性和安全性,既减少了代码中频繁使用命名空间前缀的繁琐,又降低了名称冲突的风险。
3,注释
和C语言一样
//:单行注释
/* */:多行注释
4,变量
与C语言相同
int a = 10;
#include <iostream>
using namespace std;
int main()
{int a = 10;cout << "a = " << a << endl;//输出a的值system("pause");return 0;
}
5,常量
C++定义常量的两种方式与C相同
(1),#define宏常量
(2),const修饰的常量
通常在变量定义前加关键字const,修饰该变量为常量,不可修改。
对于全局const
变量,在默认情况下,它们存储在只读数据段(.rodata)。这是因为全局const
变量的值在程序运行过程中通常是不应该被改变的,只读数据段提供了一种合适的存储方式来确保这种不变性。对于局部const
变量,其存储位置和普通局部变量类似。如果变量是基本数据类型(如int
、double
等),它通常会被存储在栈区。
#inclue <iostream>
using namespace std;
//1,#define宏常量
#define Day 7int main()
{
//2,const修饰的变量const int month = 12;//加了const之后不能再修改,否则会报错cout << "一周共有:" << Day << endl;system("pause");return 0;
}
6,关键字(标识符)
含义和C语言相同
(1),C++常见关键字
(2),标识符命名规则
- 标识符不可以是关键字
- 标识符只能由字母,数字,下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字母区分大小写
二,数据类型
数据类型方面除字符串型,其他与C语言一样
1,整型
2,sizeof关键字
#include <iostream>
using namespace std;
int main()
{short num1 = 10;cout << "short占用的内存空间为:" << sizeof(num1) << endl;system("pause");return 0;
}
3,实型(浮点型)
#include <iostream>
using namespace std;
int main()
{float f1 = 3.14f;//不加f会默认为双精度doublecout << "f1 = " << f1 << endl;system("pause");return 0;
}
用科学计数法表示小数
float f2 = 3e2;//3 * 10^2
float f3 = 3e-2;//3 * 10^(-2)
4,字符型
char ch = 'a';//单引号中只能放1个字符
C和C++中字符型变量都只占用一个字节。字符型变量并不是把字符本身存放到内存中,而是将对应的ASCII编码放到存储单元
#include<iostream>
using namespace std;
int main()
{char ch = 'a';cout << ch << endl;cout << (int)ch << endl;//输出ch所代表的ASCII值cout << "字符型变量所占内存:" << ch <<endl;system("pause");return 0;
}
5,转义字符
用于表示一些不能显示出来的ASCII字符,常用的是\n,\\,\t(一个\t输出8个空格)
#include<iostream>
using namespace std;
int main()
{cout << "hello world\n";cout << "\\" << endl;cout << "aaaa\thello" << endl;cout << "aaa\thello" << endl;//输出为aaaa hello//输出为aaa hello//可以对齐\t后面的内容system("pause");return 0;
}
6,字符串型
(1)C风格字符串
C风格字符串在C++中同样适用
char 变量名[] = "字符串";
(2)C++风格字符串
在 C++ 中,string
是一个非常有用的类,它用于处理字符串。string
类位于<string>
头文件中,相比于 C 语言中的字符数组,string
类提供了更加方便、安全和功能丰富的字符串操作方式。
1,定义和初始化string
对象
定义一个string
对象很简单。可以使用以下几种方式进行初始化:
- 无参数初始化:
std::string str;
:这种方式创建一个空的字符串对象,之后可以使用各种成员函数或者操作符来添加内容。
- 使用字符串字面量初始化:
std::string str = "Hello";
或者std::string str("Hello");
:这两种方式都会创建一个包含字符串"Hello"
的string
对象。
- 复制初始化:
- 假设有一个已存在的
string
对象str1
,可以通过std::string str2 = str1;
来创建一个与str1
内容相同的string
对象str2
。
- 假设有一个已存在的
2,字符串拼接
使用+
运算符可以很方便地进行字符串拼接。例如:
std::string str1 = "Hello";
std::string str2 = " World";
std::string str3 = str1 + str2;
std::cout << str3 << std::endl;
//输出结果为Hello World
此外,还可以使用+=
运算符来将一个字符串添加到另一个字符串的末尾。例如:
std::string str = "Hello";
str += " World";
std::cout << str << std::endl;
3, 获取字符串长度
可以使用length()
或者size()
成员函数来获取字符串的长度。这两个函数的功能相同。例如:
std::string str = "Hello";
std::cout << "字符串的长度为: " << str.length() << std::endl;
//输出结果为5
4,访问字符串中的字符
可以使用[]
运算符或者at()
成员函数来访问字符串中的单个字符。例如:
std::string str = "Hello";
std::cout << "第一个字符是: " << str[0] << std::endl;
std::cout << "第二个字符是: " << str.at(1) << std::endl;
都会输出字符串str
中的相应字符。不过,at()
函数在访问越界字符时会抛出std::out_of_range
异常,而[]
运算符在访问越界字符时可能会导致程序崩溃或者产生未定义行为。
5,字符串比较
可以使用==
、!=
、<
、>
、<=
、>=
等运算符来比较两个string
对象。这些运算符会按照字典序比较两个字符串。例如:
std::string str1 = "abc";
std::string str2 = "abd";
if (str1 < str2) {std::cout << "str1小于str2" << std::endl;
}
//输出结果:str1小于str2。
6, 字符串查找
string
类提供了多种查找函数,如find()
、rfind()
等。find()
函数用于从字符串的开头查找指定的子字符串或者字符,rfind()
函数则用于从字符串的末尾开始查找。例如:
std::string str = "Hello World";
int index = str.find("World");
if (index!= std::string::npos) {std::cout << "找到子字符串,位置为: " << index << std::endl;
} else {std::cout << "未找到子字符串" << std::endl;
}
这里npos
是string
类中的一个静态常量,表示没有找到。如果find()
函数返回的值不是npos
,则表示找到了指定的子字符串。
7,字符串替换和删除
可以使用replace()
函数来替换字符串中的一部分内容。例如:
std::string str = "Hello World";
str.replace(0, 5, "Hi");
std::cout << str << std::endl;
会输出Hi World
。replace()
函数的第一个参数和第二个参数指定了要替换的起始位置和长度,第三个参数是用来替换的内容。
要删除字符串中的一部分内容,可以使用erase()
函数。例如:
std::string str = "Hello World";
str.erase(0, 5);
std::cout << str << std::endl;
会输出World
。erase()
函数的第一个参数和第二个参数指定了要删除的起始位置和长度。
7,布尔类型bool
bool类型只有两个值:true和false
bool类型只占用1个字节大小
#include<iostream>
using namespace std;
int main()
{bool flag = true;cout << flag << endl;//输出为1.flag = false;cout << flag << endl;//输出为0.system("pause");return 0;}
三,运算符
运算符上C++和C语言相同
1,算术运算符
#include<iostream>
using namespace std;
int main()
{int a=10;int b=3;cout << a+b << endl;//输出13system("pause");return 0;
}
#include<iostream>
using namespace std;
int main()
{int a=10;int b=1;b=++a;cout << "a=" << a << endl;cout << "b=" << b << endl;//输出a=11,b=2system("pause");return 0;
}
2,赋值运算符
3,比较运算符
#include<iostream>
using namespace std;
int main()
{int a = 10;int b = 20;cout << (a == b) << endl;//优先级问题要加括号system("pause");return 0;
}
4,逻辑运算符
5,三目运算符
语法:表达式1 ? 表达式2 : 表达式3 (如果表达式1为真则执行表达式2,如果为假则执行表达式3)
如下,此运算符既可以放在赋值号左边又可以放在赋值号右边
#include<iostream>
using namespace std;
int main()
{int a = 10;int b = 20;int c = 0;c = (a>b ? a : b);//c输出为20//在C++中三目运算符返回的是变量,可以继续赋值(a>b ? a : b) = 100;//b=100,a=10system("pause");return 0;
}
四,程序流程结构
与C语言相同
1,选择结构
if语句:与C语言相同
switch语句:
switch()中的表达式判断时只能是整型或者字符型,且不可以判断一个区间
2,循环结构
while循环
while(循环条件){循环语句}
do while循环
do{循环语句}while(循环条件);
for循环
3,跳转语句
break
continue
goto
在 C++ 中,goto
语句是一种无条件跳转语句。它允许程序的执行流程跳转到程序中标记的另一个位置,这个位置由一个标签(label)来标识。标签是一个用户自定义的标识符,后面跟着一个冒号(:),它可以放在任何语句之前,用于标记goto
语句的目标位置。
goto
语句的基本语法是:goto <label>;
,其中<label>
是一个已经定义的标签。例如:
#include <iostream>
int main() {int num = 1;start:std::cout << num << std::endl;num++;if (num <= 5) {goto start;}return 0;
}
在这个例子中,start:
是一个标签,定义在std::cout << num << std::endl;
语句之前。goto start;
语句会使程序的执行流程跳转到标签start
所标记的位置,这样就形成了一个简单的循环,会输出 1 到 5 这几个数字。
缺点和注意事项
- 破坏程序结构和可读性:
goto
语句会使程序的执行流程变得混乱,特别是在大型复杂的程序中。它可能会跳过一些重要的初始化或者清理代码,导致难以理解程序的逻辑和正确的执行顺序。例如,一个包含多个goto
语句的函数可能会让其他开发者很难弄清楚程序是如何从一个部分跳到另一个部分的。 - 容易导致错误:由于
goto
语句的随意跳转特性,可能会导致变量的初始化、销毁等操作出现问题。例如,在跳转过程中可能会跳过变量的初始化语句,导致程序使用未初始化的变量,从而产生未定义行为。因此,在使用goto
语句时应该非常谨慎,并且在代码中添加足够的注释来解释goto
语句的目的和跳转后的逻辑。 - 不符合结构化编程原则:结构化编程提倡程序具有良好的顺序、选择和循环结构,
goto
语句的使用在一定程度上违背了这个原则。现代编程理念通常建议尽量避免使用goto
语句,除非在一些特殊的、经过仔细考虑的情况下,如前面提到的错误处理场景
五,数组
数组与C语言相同
1,一维数组
#include<iostream>
using namespace std;
int main()
{int arr[5];arr[0]=1;arr[1]=2;arr[2]=3;arr[3]=4;arr[4]=5;int arr1[5]={1,2,3,4,5};int arr2[]={1,2,3,4,5};system("pause");return 0;
}
sizeof(arr)可以统计整个数组的长度。一个数组内的一个整形元素占4个字节
cout << arr << end1;可以输出arr数组的首地址
cout << (int)arr <<end1; 将16进制地址转为十进制
cout << &arr[2] << end1;可以输出arr内元素的地址
2,二维数组
#include<iostream>
using namespace std;
int main(){int arr[2][3];arr[0][0] = 1;arr[0][1] = 2;//二维整型数组每个元素仍然占4个字节int arr2[2][3] = {{1,2,3},{4,5,6}};int arr3[2][3] = {1,2,3,4,5,6};int arr4[][3] = {1,2,3,4,5,6};//注意,列数不能省略system("pause");return 0;
}
cout << arr[0] << endl;可以输出二维数组第一行的首地址
cout << arr[1] << endl;可以输出二维数组第二行的首地址
要输出数组中的元素,需要将行列的索引都写上arr[0][0]则输出第一个元素
六,函数
与C语言相同
七,指针
int*p
在32位操作系统下,指针所占内存空间为4个字节
在64位操作系统下,指针所占内存空间为8个字节
sizeof(p)或者是sizeof(int*)
1,空指针和野指针
- 空指针
指针变量指向内存中编号为0的空间。主要用于给指针变量初始化
Int*p=NULL;
- 野指针
指针变量指向非法的内存空间
int*p=(int*)0x1100;
空指针指向的内存不可被访问。
2,const修饰指针
//const修饰指针。指针指向可以修改,但是指针指向的值不能修改
const int * p = &a;
*p = 20;//错误操作,指针指向的值不能改
p = &b;//正确操作,指针指向可以修改
//const修饰常量--指针常量。指向不可以改,指向的值可以改
int * const p = &a;
*p = 20;//正确,指向的值可以改
p = &b;//错误,指向不可以改
//const既修饰指针,又修饰常量.指向和指向的值都不可以改
const int * const p = &a;
*p = 20;//错误
p = &b;//错误
3,指针和数组
#include<iostream>
using namespace std;
int main()
{int arr[5] = {1,2,3,4,5};int *p = arr;//*p存放数组第一个元素地址,即首地址p++;//现在p指向了第二个元素system("pause");return 0;
}
4,指针和函数
void swap(int *p1,int*p2);
八,结构体
1,结构体定义
结构体属于用户自定义数据类型,允许用户存储不同的数据类型
struct 结构体名{结构体成员列表}; //结构体名就是数据类型
#include<iostream>
#include<string>//使用字符串必须包含此头文件
using namespace std;
int main()
{ //方式1struct Student{string name;int age;int score; };struct Student s1;//创建变量时struct可以省略//方式2struct Student s2 = {string name;int age;int score; };//方式3struct Student{string name;int age;int score; }s3;s1.name = "zzz"s1.age = 18;s1.score = 100;//通过.访问属性cout << "姓名:" << s1.name << "年龄" << s1.age << "分数:" << s1.score << endl; system("pause");return 0;
}
2,结构体数组
将自定义的结构体放入数组中方便维护
语法:struct 结构体名 数组名[元素个数] = {{},{},{}.......}
#include<iostream>
#include<string>
using namespace std;
struct Student
{string name;int age;int score;
};
int main()
{ struct Student stuArray[3]={{"张三",18,100},{"李四",28,99},{"王五",38,66}};//不赋值也可以。struct可以省略stuArray[2].name = "赵六";//可以通过.来修改或赋值system("pause")return 0;
}
3,结构体指针
通过指针访问结构体中的成员
利用操作符->可以通过结构体指针访问结构体属性
#include<iostream>
#include<string>
using namespace std;
struct Student
{string name;int age;int score;
}
int main()
{ Student s={"张三",18,100};//struct可以省略Student*p = &s;//通过指针指向结构体变量,Student前也可以加上structp->age;//通过指针访问结构体变量中的数据system("pause");return 0;
}
4,结构体嵌套结构体
#include<iostream>
#include<string>
using namespace std;
struct student
{string name;int age;int score;
}
struct teacher
{int id;string name;int age;struct student stu;
}
int main()
{ teacher t;t.id = 10000;t.name = "老王";t.age = 50;t.stu.name = "小王";t.stu.age = 20;t.stu.score = 60;system("pause");return 0;
}
5,结构体做函数参数
将结构体作为参数向函数传递
传递方式有值传递和地址传递两种
#include<iostream>
#include<string>
using namespace std;
struct Student
{string name;int age;int score;
}
//值传递
void printStudent1(struct student s)
{cout << "姓名" << s.name << "年龄" << s.age << "分数" << s.score << endl;
}
//地址传递
void printStudent2(struct student*p)
{cout << "姓名" << p->name << "年龄" << p->age << "分数" << p->score << endl;
}
int main()
{struct student s;s.name = "张三";s.age = 20;s.score = 85;system("pause");printStudent1(s);printStudent1(&s);return 0;
}
6,结构体中const使用场景
#include<iostream>
#include<string>
using namespace std;
struct Student
{string name;int age;int score;
}
//值传递
void printStudent1(struct student s)
{cout << "姓名" << s.name << "年龄" << s.age << "分数" << s.score << end1;
}
//地址传递
void printStudent2(const struct student*p)
{cout << "姓名" << p->name << "年龄" << p->age << "分数" << p->score << end1;
}
int main()
{struct student s;s.name = "张三";s.age = 20;s.score = 85;system("pause");printStudent1(s);printStudent1(&s);return 0;
}
九,程序的内存模型
在程序编译后,生成exe可执行程序,未执行该程序前分为两个区域代码区和全局区
1,代码区
2,全局区
//字符串常量的地址,此常量在全局区
cout << "hello world的地址为:" << &"hello world" << end1
程序运行后的区域为栈区
3, 栈区
#include<iostream>
using namespace std;
int*func()
{int a = 10;//局部变量存放在栈区,栈区数据在函数执行完后自动释放return &a;//返回局部变量地址
}
int main()
{int *p = func();cout << *p << end1;//第一次输出正常,输出10,是因为编译器做了保留cout << *p << end1;//第二次输出乱码。第二次数据就不会保留了system("pause");return 0;
}
4,堆区
#include<iostream>
using namespace std;
int*func()
{//利用new关键字将数据开辟到堆区//指针本质也是局部变量,放在栈区,指针保存的数据是放在堆区int*p = new int(10);//返回值为开辟的地址所以可以用int *p接收return p;
}
int main()
{int*p = func();cout << *p <<end1;cout << *p <<end1;cout << *p <<end1;//都会输出正确答案10system("pause");return 0;
}
#include<iostream>
using namespace std;
int*func()
{//利用new挂念子将数据开辟到堆区//指针本质也是局部变量,放在栈区,指针保存的数据是放在堆区int*p = new int(10);//返回值为开辟的地址所以可以用int *p接收return p;
}
int main()
{int*p = func();cout << *p <<end1;delete p;//释放内存,下面会出现乱码cout << *p <<end1;cout << *p <<end1;system("pause");return 0;
}
用new在堆区开辟数组
void test01()
{int*arr = new int[10];//代表数组有10个元素。返回数组首地址for(int i=0;i<10;i++){arr[i] = i;//赋值操作}for(int i=0;i<10;i++){cout << arr[i] <<end1;//输出操作}delete[] arr;//释放数组要加一个[]
}
十,引用
1,基本语法
作用:给变量起别名
语法:数据类型 &别名 = 原名
实际上就是 数据类型 *const 别名 = &a
起别名后原名和别名都能操纵同一块内存,用别名和原名修改数据则输出值都会修改
#include<iostream>
using namespace std;
int main()
{int a = 10;int &b = a;b = 100;cout << "a=" << a << end1;cout << "b=" << b << end1;//a,b均输出100system("pause");return 0;
}
注意:
引用必须初始化。引用在初始化后,指向不可以改变,值可以改变。
int &b;//错误,必须初始化
int &b = a;&b = c;//错误,初始化后指向不可以再改变
2,引用做函数参数
#include<iostream>
using namespace std;
//交换函数(引用方法)
void mySwap(int &a, int &b)
{int temp = a;a = b;b = temp; }
int main()
{int a = 10;int b = 20;mySwap(a,b);//a,b会发生交换,也就是形参可以修饰实参cout << "a=" << a << endl;cout << "b=" << b << endl;system("pause");return 0;
}
3,引用做函数返回值
引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
#include<iostream>
using namespace std;
int& test01()
{int a = 10;return a;//返回局部变量引用
}
int main()
{int &ref = test01();cout << "ref=" << ref << endl;cout << "ref=" << ref << endl;//第一次正确,第二次出现乱码,所以不能返回局部变量引用system("pause");return 0;
}
int &test02()
{static int a = 10;return a;
}
int main()
{int &ref = test02();cout << "ref=" << ref << endl;cout << "ref=" << ref << endl;//均输出正确,输出10test02() = 1000;//函数调用可以作为左值cout << "ref=" << ref << endl;//输出1000system("pause");return 0;
}
//如果函数的返回值为引用,这个函数的调用可以作为左值
4,引用的本质
引用的本质在C++中是一个指针常量,就相当于数据类型 *const 别名 = &a
5,常量引用
#include<iostream>
using namespace std;int main()
{ //加上const后,编译器将代码修改int temp = 10; int & ref = temp;const int & ref = 10;ref = 20;//错误,不可修改。加入const变为只读,不可以修改cout << "ref=" << ref << end1;cout << "ref=" << ref << end1;system("pause");return 0;
}
#include<iostream>
using namespace std;
void showValue(const int &val)
{ val = 1000;//错误,不可修改cout << "val=" << val << end1;
}
int main()
{ int a = 100;showValue(a);cout << "ref=" << ref << end1;system("pause");return 0;
}
十一,函数高级
1,函数默认参数
C++中,函数的形参是可以有默认值的
语法:返回值类型 函数名(参数 = 默认值){}
#include<iostream>
using namespace std;
int func(int a, int b =20, int c =30)
{return a+b+c;
}
int main()
{ cout << func(10, 30) << end1;//输出70system("pause");return 0;
}
注意事项:1,如果某个位置已经有了默认参数,那么从这个位置往后都必须要有默认值
int func(int a, int b =20, int c)//错误,c必须要有默认参数
{return a+b+c;
}
//声明和函数实现只能有一个位置有默认参数
2,如果函数的声明有了默认参数,函数的实现就不能有默认参数
int func(int a = 10, int b = 10);//声明
int func(int a = 10, int b = 10)//错误,声明已经有默认参数了,此处不能加默认参数
{return a+b+c;
}
2,函数的占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该参数
#include<iostream>
using namespace std;
void func(int a, int)//占位参数int
{cout << "sssss" << end1;
}
int main()
{ func(10,10);//必须填补该参数system("pause");return 0;
}
占位参数也可以有默认参数,比如
void func(int a, int = 10)//占位参数int
{cout << "sssss" << end1;
}
3 ,函数重载
函数名可以相同,提高复用性
#include<iostream>
using namespace std;
//1,以下两个函数都在全局作用域下
//2,函数名称相等
//3,函数参数类型不同,或者个数不同,或者顺序不同
//也就是通过形参区分同名函数
void func(int a)
{cout << "sssss" << end1;
}
void func()
{cout << "ccccc" << end1;
}
void func(double a)
{cout << "aaaaa" << end1;
}
int main()
{ func(10);system("pause");return 0;
}
注意事项:1,引用作为重载条件 2,函数重载碰到函数默认参数
#include<iostream>
using namespace std;
void func(int &a)
{cout << "sssss" << end1;
}
void func(const int &a)//可以重载属于类型不同
{cout << "sssss" << end1;
}
int main()
{ int a = 10;func(a);//调用不加const的函数,func(10);//调用加const的函数system("pause");return 0;
}
#include<iostream>
using namespace std;
void func(int a, int b = 10)
{cout << "sssss" << end1;
}
void func(int a)
{cout << "ccccc" << end1;
}
int main()
{ func(10);//两个函数都能调用,出现错误system("pause");return 0;
}
十二,类和对象
C++面向对象的三大特性为:封装,继承,多态
对象有其属性和行为
1,封装
封装将属性作为一个整体,将属性和行为加以权限控制
在设计类时,属性和行为写在一起。类中的属性和行为都叫成员,叫成员属性,成员变量。行为叫成员方法
语法:class 类名{访问权限:属性/行为};
代码演示
//实例一:设计一个圆的类,并求圆的周长
#include<iostream>
using namespace std;
const double PI = 3.14
class Circle//创建类名
{//访问权限
public://公共权限//属性int m_r;//半径//行为double calculateZC()//获取圆的周长{return 2 * PI * m_r;}
};
int main()
{//通过圆类 创建具体的圆(对象) 即实例化Circle c1;//给圆对象的属性进行赋值cl.m_r = 10;system("pause");return 0;
}
//实例二:设计一个学生类,属性有姓名和学号,可以给姓名和学号进行赋值,可以显示学生的姓名和学号
#include<iostream>
#include<string>
using namespace std;
class Student
{
public://公共权限//属性string m_Name;//姓名int m_Id;//学号void showStudent(){cout << "姓名:" << m_Name << "学号:" << m_Id << end1;}//也可以直接在类中给属性赋值void setName(string name){m_Name = name;}}
int main()
{Student s1;s1.setName("张三");s1.m_Name = "张三";s1.m_Id = 1;//显示学生信息s1.showStudent;}
访问权限
#include<iostream>
using namespace std;
//公共权限 public:成员在类内可以访问,类内也可以访问
//保护权限 protected:成员类内可以访问,类外不可以访问,子类也可以访问父类中的保护内容
//私有权限 private:成员类内可以访问,类外不可以访问,子类不可以访问父类中的保护内容
class Person
{
public://公共权限string m_Name;
protected://保护权限string m_Car;
private://私有权限int m_Password;
public:void func(){m_Name = "张三";m_Car = "拖拉机";m_Password = "123456";//类内都可以访问}
};
int main()
{//实例化具体对象Person p1;p1.m_Name = "李四";//正确,公共权限类外可以访问p1.m_Car = "奔驰";//错误,保护权限类外不可访问}
struct和class区别
#include<iostream>
using namespace std;
class C1
{int m_A;
};
struct C2
{int m_A;
};
int main()
{ //struct默认权限为公共//class默认权限是私有C1 c1;c1.m_A=100;//错误,class中不指定权限默认为私有,此处不可访问C1 c2;c2.m_A=100;//正确,struct默认权限为公共system("pause");return 0;
}
成员属性设置为私有
优点一:可以自己控制读写权限
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:void setName(string name){m_Name = name;}string getName(){return m_Name; }int getAge(){retrun m_Age;}void srtIdol(string idol){m_Idol = idol;}
private:string m_Name;//姓名 可读可写int m_Age = 18;//年龄 只读sting m_Idol;//偶像 只写
}
int main()
{Person p;p.setName("张三");p.getName();p.getAge;p.setIdol("小明");system("pause");return 0;
}
优点二:对于写操作可以验证数据有效性
案例学习:如果要比较两个类的对象,可以利用全局函数,给全局函数传入两个参数,这两个参数就是类创建的对象,如下
#include<iostream>
using namespace std;
class Cude
{};
void compare(Cude c1,Cude c2)
{}
int main()
{ Cude c1;Cude c2;compare(c1, c2);system("pause");return 0;
}
也可以利用成员函数进行判断,成员函数进行判断可以省略一个参数,因为可以直接调用类里的属性作为一个对象,如下
#include<iostream>
using namespace std;
class Cude
{void compare(Cude c1){if(age = c1.age){}}
private:int age = 18;
};
int main()
{ Cude c1;Cude c2;c1.compare(c2);system("pause");return 0;
}
2,对象特性(对象的初始化和清理)
(1)构造函数和析构函数(对象初始化和清理)
构造函数在类调用开始时自动调用,析构函数在类调用结束时自动调用(注意,在函数结束销毁时才会调用)
如下,注意这两个函数编译器会自动调用,不写也会运行,只不过函数内部是空的
#include<iostream>
using namespace std;
class Person
{
public://构造函数,可以有参数Person(){cout << "sss" << end1;}//析构函数,不能有参数~Person(){cout << "zzz" << end1;}
};
int main()
{system("pause");return 0;
}
(2)构造函数的分类及调用
#include<iostream>
using namespace std;
class Person
{
public:Person()//无参构造(默认构造)函数{cout << "sss" << endl;}Person(int a)//有参构造函数{ age = a;cout << "sss" << endl;}//以上两种均为普通构造函数。//拷贝构造函数,参数为类对象,意思是将此类对象的所有属性都拷贝到此对象中Person(const Person &p)//固定参数写法{age = p.age;//将传入对象身上的所有属性拷贝到此对象身上}~Person(){cout << "zzz" << endl;}int age;
};
//调用
void test01()
{//1,括号法Person p1;//无参构造函数调用Person p2(10);//有参构造函数调用Person p3(p2);//拷贝构造函数调用。将对象p2的属性拷贝到p3中//注意事项:调用无参构造函数时不能加括号//2,显示法Person p1;//有参构造Person p2 = Person(10);//有参构造Person p3 = Person(p2); //Person(10)叫做匿名对象。匿名对象在当前行执行结束后,系统会立即回收匿名对象//所以用Person p1来接收//注意事项:不要利用拷贝构造函数初始化匿名对象。即不能将Person(p2)放在单独一行//3,隐式转换法Person p4 = 10;//相当于Person p4 = Person(10)Person p5 = p2;
}
int main()
{system("pause");return 0;
}
(3)拷贝函数调用时机
#include<iostream>
using namespace std;
class Person
{
public:Person(){}Person(int age){}Person(const Person &p){}~Person(){}};
//1,使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{Person p1(20);Person p2(p1);}
//2,值传递的方式给函数参数赋值
void doWork(Person p)
{}
void test02()
{Person p;doWork(p);//当给函数传输对象时,会调用拷贝函数将p的数据复制一份给doWork作为形参//也就是在doWork函数里改变p的属性值并不会改变函数外p的属性值}
//3,值方式返回局部对象
Person doWork2()
{Person p1;//局部对象在函数结束后就会释放掉。return p1;//系统会调用拷贝函数创建一个新的对象p1作为返回值返回
}
void test03()
{Person p = doWork02();
}
(4)构造函数调用规则
也就是说不在类中写拷贝函数就可以调用拷贝函数进行拷贝
(5)深拷贝与浅拷贝
#include<iostream>
using namespace std;
class Person
{
pubilc:Person(){}Person(int age){m_Age = age;}~Person(){}int m_Age;};
void test01()
{Person p1(18);Person p2(p1);//我们未写拷贝函数,编译器提供的拷贝函数为值拷贝}
#include<iostream>
using namespace std;
class Person
{
pubilc:Person(){}Person(int age, int height){m_Age = age;m_Height = new int(height);}~Person(){//析构代码,将堆区开辟数据做释放if(m_Height != NULL){delete m_Height;m_Height = NULL;}}int m_Age;int *m_Height;//身高数据要开辟到堆区,所以用指针};
void test01()
{Person p1(18);Person p2(p1);//我们未写拷贝函数,编译器提供的拷贝函数为值拷贝}
浅拷贝会导致堆区内存重复释放
在 C++ 中,浅拷贝通常是指对象之间的成员逐个赋值,即只复制对象的成员变量的值,而不复制成员变量所指向的资源。例如,如果一个类有一个指针成员变量,浅拷贝只会复制这个指针的值,而不会复制指针所指向的内存区域。这种方式在很多情况下可能会导致问题,因为多个对象可能会指向同一块内存区域。
当进行浅拷贝时,如果两个或多个对象都指向同一块动态分配的内存,那么在对象的析构函数被调用时,就可能会出现多次释放同一块内存的情况。例如,假设有一个类 A,其中有一个指针成员变量指向动态分配的内存。当进行浅拷贝时,新创建的对象也会有一个指针指向相同的内存区域。如果这两个对象的析构函数都被调用,那么就会对同一块内存进行两次释放6。
此问题需要深拷贝来解决。自己写拷贝函数,重新在堆区申请一块内存,并保存数据为与p1相同。让p2指向此内存
如下
Person(const Person&p)
{m_Age = p.m_Age;m_Height = new int(*p.m_Height);}
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
(6)初始化列表
class Person
{
public://传统方式初始化属性Person(int a,int b,int c){m_A = a;m_B = b;m_C = c;}//初始化列表初始化属性.这样的话m_A,m_B,m_C写死了不能再修改了Person():m_A(10),m_B(20),m_C(30){}//初始化列表初始化属性.m_A,m_B,m_C可以修改Person(int a,int b,int c):m_A(a),m_B(b),m_C(c){}int m_A;int m_B;int m_C;
}
(7)类对象作为类成员
C++中的成员可以是另一个类的对象,我们称该成员为对象成员
当其他类对象作为本类成员构造时候先构造其他类对象,再构造自身,析构的顺序与构造相反
#include<iostream>
#include<string>
using namespace std;
class Phone
{
pubic:Phone(string pName){m_PName = pName;}string m_Pname;
};
class Person
{
public://这里m_Phone(pName)相当于Phone m_Phone = pName;使用了隐式转换法Person(string name, string pName):m_Name(name), m_Phone(pName){}string m_Name;Phone m_Phone;
}
void test01()
{Person P("张三", "苹果MAX");cout << p.m_Name << "拿着:" << p.m_Phone.m_PName << endl;
}
int main()
{test01();system("pause");return 0;
}
(8)静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。
静态成员分为两种:静态成员变量和静态成员函数
静态成员变量
- 加上static的成员变量所有对象的此值都一样。比如static a = 100;对象p1的a等于100,而对象p2将此值改为了200,那么对于p1来说,这个值也将变为200,即所有的对象共用这一个值。
- 加了static的成员变量在编译阶段就分配了内存。分配到了全局区
- 必须类内声明,类外初始化。此数据必须要有一个初始值。如下:
#include<iostream>
using namespace std;
class Person
{
public:static int m_A;//类内声明};int Person::m_A = 100;//类外初始化void test01()
{Person p;cout << p.m_A << endl;Person p2;p2.m_A = 200; //别的对象可以修改static的值,且所有对象对应的此值均改变cout << p.m_A << endl;
}void main()
{test01();//输出为100和200
}
void test02()
{//静态成员变量不属于某个对象上,所有对象都共享同一份数据//因此静态成员变量有两种访问方式//1,通过对象就行访问Person p;cout << p.m_A << endl;//2,通过类名进行访问cout Person::m_A << endl;
}
对于非静态成员变量,在类外则只能先创建对象再通过对象进行访问
//对于有访问权限的静态成员变量
class Person
{
private:static int m_B;
};int Person::m_B = 200;void test03()
{cout << Person::m_B << endl;//错误,私有作用域下不能访问
}
静态成员函数
与静态成员变量类似,所有对象共用同一个函数。
除此之外,还有一个比较重要的特点:静态成员函数只能访问静态成员变量
#include<iostream>
using namespace std;
class Person
{
public:static void func(){m_A = 100;//静态成员函数可以访问静态成员变量m_B = 200;//错误,不可以访问非静态的成员变量cout << "static void func调用" << endl;}static int m_A;//静态成员变量int m_B;//非静态成员变量};
int Person::m_A = 0;void test01()
{//两种访问静态成员函数的方法//1,通过对象访问Person p;p.func();//2,通过类名访问Person::func();
}void main()
{test01();
}
与静态成员变量一样,静态成员函数也有访问权限
对于非静态成员函数,在类外则只能先创建对象再通过对象进行访问
3,C++对象模型和this指针
(1)成员变量和成员函数分开存储
虽然成员变量和成员函数都是在类中的,但是实际上他们在类中是分开存储的
只有非静态成员变量才属于类的对象上。也就是上述说的静态成员变量不属于类中的对象。非静态成员函数也不属于类的对象上
#include<iostream>
using namespace std;
class Person
{
};
void test01()
{Person p;// 空对象占用内存空间为1//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占用的空间。cout << "size of p =" << sizeof(p) << endl;
}void main()
{test01();
}
#include<iostream>
using namespace std;
class Person
{ int m_A;//非静态成员变量
};
void test01()
{Person p;cout << "size of p =" << sizeof(p) << endl;// 对象占用内存空间为4。就是int的内存大小// 说明非静态成员变量属于类的对象上
}void main()
{test01();
}
#include<iostream>
using namespace std;
class Person
{ int m_A;//非静态成员变量static int m_B;
};
int Person::m_B = 0;void test01()
{Person p;cout << "size of p =" << sizeof(p) << endl;// 对象占用内存空间为4。就是int的内存大小// 说明静态成员变量不属于类的对象上
}void main()
{test01();
}
#include<iostream>
using namespace std;
class Person
{ int m_A;//非静态成员变量void func(){}
};
void test01()
{Person p;cout << "size of p =" << sizeof(p) << endl;// 对象占用内存空间为4。就是int的内存大小// 说明静态成员函数不属于类的对象上。当然静态成员函数也不属于类的对象上
}void main()
{test01();
}
(2)this指针
上一节我们得知了c++中成员变量和成员函数是分开存储的。每一个非静态成员函数是多个同类型对象所共用的,但是,非静态成员函数是只能通过对象来访问,那么它是如何区分是哪个对象调用的自己?
C++通过提供特殊的对象指针,this指针来解决上述问题。this指针指向被调用的成员函数所属的对象。p1调用成员函数了,this就指向p1,p2调用成员函数了,this就指向p2
this指针是隐含在每一个非静态成员函数内的一种指针,也就是他不需要定义直接使用即可
this指针的用途:
- 在形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可以使用return *this
#include<iostream>
using namespace std;
class Person
{
public:Person(int age){age = age;}int age;
};
//1,解决名称冲突
void test01()
{Person p1(18);cout << "p1的年龄为:" << p1.age << endl;
}int main()
{test01();
}
上述代码运行后输出的 p1的年龄为:-41824821 数字乱码。这是因为编译器认为构造函数的参数age和函数里的两个age都是同一个东西,与成员变量age没关系,所以就没有给属性age赋值过,所以会出现乱码
解决办法:
- 写代码规范性:将所有成员变量都改为m_age,在前面加一个下划线,代表属于member
- 加this->,如下
#include<iostream>
using namespace std;
class Person
{
public:Person(int age){//this指针指向 被调用的成员函数 所属的对象//即创建p1时指向p1this->age = age;}int age;
};
//1,解决名称冲突
void test01()
{Person p1(18);cout << "p1的年龄为:" << p1.age << endl;
}int main()
{test01();
}
作用二:在类的非静态成员函数中返回对象本身,可以使用return *this
#include<iostream>
using namespace std;
class Person
{
public:Person(int age){//this指针指向 被调用的成员函数 所属的对象//即创建p1时指向p1this->age = age;}void PersonAddAge(Person &p){this->age += p.age;}int age;
};//2,返回对象本身
void test02()
{Person p1(10);Person p2(10);//如果要加1个10岁,如下调用就可以p2.PersonAddAge(p1)//如果想要多加几次10岁,如下肯定不行。因为PersonAddAge(p1)没有返回值//p2.PersonAddAge(p1)就不能再调用PersonAddAge(p1)。//除非p2.PersonAddAge(p1)返回的还是对象p2,就可以再调用函数PersonAddAge(p1)p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
}int main()
{test01();
}
如下为 p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);的正确调用方式
#include<iostream>
using namespace std;
class Person
{
public:Person(int age){//this指针指向 被调用的成员函数 所属的对象//即创建p1时指向p1this->age = age;}Person& PersonAddAge(Person &p){this->age += p.age;//this指向p2的指针,而*this指向的就是p2这个对象return *p2;}int age;
};//2,返回对象本身
void test02()
{Person p1(10);Person p2(10);//如果要加1个10岁,如下调用就可以p2.PersonAddAge(p1)//如果要加多个10岁,如下调用就可以//链式编程p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
}int main()
{test01();
}
(3)空指针访问成员函数
C++允许空指针调用成员函数,但要注意是否用到this指针
如果用到this指针需要加以判断
#include<iostream>
using namespace std;class Person
{
public:void showClassName(){cout << "this is Person class" << endl;}void showPersonAge(){cout << "age = " << m_Age << endl;}int m_Age;};void test01()
{Person* p = NULL;p -> showClassName();p -> showPersonAge();
}
上述代码运行后出错。将p -> showPersonAge();注释后程序运行正常,说明调用showPersonAge函数时出错了。这是因为第二个函数中用到了m_Age,这是一个成员属性,其实在成员属性前面都默认加了this->,但是指针p指向的Person是一个空指针,this也就是一个空的东西,所以会报错。
为了防止这种问题出现,通常在成员函数前加if判断this是否为空,如下
void showPersonAge()
{if(this == NULL) return;cout << "age = " << m_Age << endl;
}
(4)const修饰成员函数
成员函数后加const我们称这个函数为常函数。常函数内不能修改成员属性。但是成员属性声明时加上关键字mutable后,在常函数中依然可以修改
#include<iostream>
using namespace std;class Person
{
public:void showPerson() const{m_A = 100;//错误,不可以修改成员属性//首先在函数体内部都会有this指针,m_A就相当于this->m_A//this指针本质是指针常量(指针常量的指向不可以修改,但指向的值可以修改)//Person* const this;//如果再加一个const,变为const Person* const this;那么值和指向都不可以改变//函数后面加const就相当于上述在Person* const this前加的const//在成员函数后面加const,修饰的是this的指向,让指向的值也不能修改m_B = 100;}int m_A;mutable int m_B;//特殊变量,即使在常函数中也可以修改此值
};
(5)const修饰对象
声明对象前加const则称该对象为常对象。常对象只能调用常函数
#include<iostream>
using namespace std;class Person
{
public:void showPerson() const{m_B = 100;}int m_A;mutable int m_B;
};viod test02()
{const Person p;//常对象p.m_A = 100;//错误,不可以修改p.m_B = 100;//正确,可以修改//常对象只能调用常函数p.showPerson();//正确
}
4,友元
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类访问另一个类中私有成员
友元的三种实现方法:
- 全局函数做友元
- 类作友元
- 成员函数做友元
(1)全局函数做友元
#include<iostream>
#include<string>
using namespace std;
class Building
{
pubilc:Building(){m_SittingRoom = "客厅";m_BedRoom = "卧室"; }
public:string m_SittingRoom;
private:string m_BedRoom;
};//全局函数
void goodGay(Building &building)
{cout << "好基友的全局函数正在访问:" << building->m_SittingRoom << endl; //类外可以访问cout << "好基友的全局函数正在访问:" << building->m_BedRoom << endl; //私有属性不能访问
}void test01()
{Building building;goodGay(building);
}
要想在全局函数里访问类中的私有属性,需要将全局函数声明在类的最上面,并在前面加上friend,如下
#include<iostream>
#include<string>
using namespace std;
class Building
{friend void goodGay(Building &building);
pubilc:Building(){m_SittingRoom = "客厅";m_BedRoom = "卧室"; }
public:string m_SittingRoom;
private:string m_BedRoom;
};//全局函数
void goodGay(Building &building)
{cout << "好基友的全局函数正在访问:" << building->m_SittingRoom << endl; //类外可以访问cout << "好基友的全局函数正在访问:" << building->m_BedRoom << endl; //私有属性不能访问
}void test01()
{Building building;goodGay(building);
}
(2)类作友元
让一个类可以访问另一个类中的私有成员
#include<iostream>
#include<string>
using namespace std;
class Building;//提前声明有Building这个类,防止GoodGay中新建building对象出错
class GoodGay
{
public:GoodGay;void visit();//用此函数访问Building类中的属性Building*building;
};
class Building
{friend class GoodGay;
public:Building();
public:string m_SittingRoom;
private:string m_BedRoom;
};//类外写成员函数
Building::Building()
{ m_SittingRoom = "客厅";m_BedRoom = "卧室";
}GoodGay::GoodGay()
{building = new Building;
}void GoodGay::visit()
{cout << "好基友类正在访问:" << building->m_SittingRoom << endl;cout << "好基友类正在访问:" << building->m_BedRoom << endl;
}void test01()
{GoodGay gg;gg.visit();
}
(3)成员函数做友元
#include<iostream>
#include<string>
using namespace std;
class Building;//提前声明有Building这个类,防止GoodGay中新建building对象出错
class GoodGay
{
public:GoodGay();void visit();//用此函数访问Building类中的私有内容void visit2();//让此函数不可以访问Building类中的私有内容Building*building;
};
class Building
{friend void GoodGay::visit();
public:Building();
public:string m_SittingRoom;
private:string m_BedRoom;
};//类外实现成员函数
Building::Building()
{ m_SittingRoom = "客厅";m_BedRoom = "卧室";
}GoodGay::GoodGay()
{building = new Building;
}void GoodGay::visit()
{cout << "好基友类正在访问:" << building->m_SittingRoom << endl;cout << "好基友类正在访问:" << building->m_BedRoom << endl;
}
void GoodGay::visit2()
{cout << "好基友类2正在访问:" << building->m_SittingRoom << endl;cout << "好基友类2正在访问:" << building->m_BedRoom << endl;//错误,无法访问
}void test01()
{GoodGay gg;gg.visit();gg.visit()2;
}
5,运算符重载
即对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型。
对于内置的数据类型(int float double等),编译器知道如何进行运算。对于自己定义的数据类型(比如类class,结构体struct等等),编译器不知道如何进行运算,这时就需要我们对已有的运算符进行重新定义,赋予其另一种功能
(1)加号运算符重载
实现两个自定义数据类型相加的运算
比如实现以下两个类的相加编译器就不知如何操作
#include<iostream>
#include<string>
using namespace std;class Person
{
public:int m_A;int m_B;
};void test01(){Person p1;p1.m_A = 10;p1.m_B = 10;Person p2;p2.m_A = 10;p2.m_B = 10;Person p3 = p1 + p2;
}
这时,我们可以考虑自己写成员函数实现两个类的相加
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;
}
编译器给这个函数换了个名字 ,统一叫做operator+,这样上述函数就变成了
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;
}
这样就通过成员函数重载了+
写法就可以变为Person p3 = p1.operator+(p2); 简化为Person p3 = p1+p2;
除此之外,也可以通过全局函数重载+运算符,如下
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 p3 = operator+(p1, p2); 简化为Person p3 = p1+p2;
由此得到如下总体代码
#include<iostream>
#include<string>
using namespace std;class Person
{
public://1,通过成员函数重载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 test01(){Person p1;p1.m_A = 10;p1.m_B = 10;Person p2;p2.m_A = 10;p2.m_B = 10;Person p3 = p1 + p2;
}
#include<iostream>
#include<string>
using namespace std;class Person
{
public:int m_A;int m_B;
};//2,全局函数重载+
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 test01(){Person p1;p1.m_A = 10;p1.m_B = 10;Person p2;p2.m_A = 10;p2.m_B = 10;Person p3 = p1 + p2;
}
运算符重载也可以发生函数重载。比如上述 Person p3 = p1 + 10;会报错,一个类加上一个整数。如下使用函数重载就可以实现这个功能
#include<iostream>
#include<string>
using namespace std;class Person
{
public:int m_A;int m_B;
};//2,全局函数重载+
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 test01(){Person p1;p1.m_A = 10;p1.m_B = 10;Person p2;p2.m_A = 10;p2.m_B = 10;Person p3 = p1 + p2;Person p3 = p1 + 10;
}
运算符重载对于内置数据类型的表达式的运算符是不可以改变的。
(2)左移运算符重载
作用:可以输出自定义数据类型
之前学的cout都是cout <<输出整型,浮点型,字符串等等。对于Person类建立的p对象,它有两个属性:p.m_A = 10; p.m_B = 10;。直接输出p:cout << p <<endl;肯定不行。
为了输出p时将p的属性顺便输出,要重载符号<<。重载的方式也有两种:成员函数和全局函数,但是成员函数重载<<会导致cout出现在<<右边,所以通常不使用成员函数重载
#include<iostream>
#include<string>
using namespace std;class Person
{
public:int m_A;int m_B;
};//cout属于输出流的类型,且只能有一个,所以用引用的方式
ostream & operator<<(ostream &cout, Person p)//本质上是operator<<(cout, p),简化为cout << p
{cout << "m_A = " << p.m_A << "m_B" << p.m_B;return cout;//这样才能保证cout << p还是一个cout对象,后面可以继续追加<<
}void test01()
{Person p;p.m_A = 10;p.m_B = 10;cout << p <<endl;//endl用来换行}
如果m_A,m_B为私有属性,则通过友元可以访问,如下
#include<iostream>
#include<string>
using namespace std;class Person
{friend ostream & operator<<(ostream &cout, Person p);
public:Person(int a, int b){m_A = a;m_B = b;}
public:int m_A;int m_B;
};//cout属于输出流的类型,且只能有一个,所以用引用的方式
ostream & operator<<(ostream &cout, Person p)//本质上是operator<<(cout, p),简化为cout << p
{cout << "m_A = " << p.m_A << "m_B" << p.m_B;return cout;//这样才能保证cout << p还是一个cout对象,后面可以继续追加<<
}void test01()
{Person p(10, 10);cout << p <<endl;//endl用来换行}
(3)递增运算符重载
通过递增运算符重载,实现自己的整型数据
#include<iostream>
using namespace std;//自定义整型
class MyInteger
{
public:MyInteger(){m_Num = 0;}//重载前置++运算符MyInteger& operator++()//返回引用是为了一直对一个数据进行递增{m_Num++;return *this;//将自身做一个返回,从而能cout输出}//重载后置++运算符MyInteger operator++(int) //int为占位参数。函数重名,加int为了区分前置++运算符函数//一定返回的是值不是引用,因为temp为局部变量,会销毁。{MyInteger temp = *this;m_Num++;return temp;}
private:int m_Num;
};void test01()
{MyInteger Myint;//自定义整型,不能直接用于输出cout << ++Myint << endl;//输出1cout << ++(++Myint) << endl;//如果返回的是引用则输出2,如果返回的只是MyInteger则为1
}void test02()
{MyInteger Myint;cout << Myint++ << endl;
}
(4)赋值运算符重载
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝的问题
#include<iostream>
using namespace std;class Person
{
public:Person(int age){m_Age = new int(age);}
public:int *m_Age;
};void test01()
{Person p1(18);Person p2(20);p2 = p1; //赋值操作,类自带。值拷贝cout << "p1的年龄为:" << *p1.m_Age << endl;//输出18cout << "p2的年龄为:" << *p2.m_Age << endl;//输出18
}
以上运行结果看起来没问题。但是堆区数据需要程序员手动释放。释放在析构函数中
#include<iostream>
using namespace std;class Person
{
public:Person(int age){m_Age = new int(age);} ~Person(){if(m_Age!=NULL){delete m_Age;m_Age = NULL;}}
public:int *m_Age;
};void test01()
{Person p1(18);Person p2(20);p2 = p1; //赋值操作,类自带。值拷贝cout << "p1的年龄为:" << *p1.m_Age << endl;//输出18cout << "p2的年龄为:" << *p2.m_Age << endl;//输出18
}
加了析构函数的代码运行后会打印出结果,但是程序也会崩掉。这是因为p2 = p1这个操作是值拷贝,他会将p1所有的数据做一个值拷贝,将p1数据指针所指向的内存也拷贝到了p2,所以p2指向的内存空间和p1相同了,当p2执行delete释放空间的操作时,将其指向的内存空间释放一次,而p1同样也释放这一块内存,这就导致同一块内存被连续释放两次。程序就会崩溃
解决方案:利用深拷贝解决浅拷贝带来的问题。重载赋值运算符
#include<iostream>
using namespace std;class Person
{
public:Person(int age){m_Age = new int(age);}~Person(){if(m_Age!=NULL){delete m_Age;m_Age = NULL;}}//重载赋值运算符void operator+(Person&p){//编译器提供的浅拷贝就是m_Age = p.m_Age;//应该先判断是否有属性在堆区,如果有就先释放干净,然后再深拷贝if(m_Age != NULL){delete m_Age;m_Age = NULL;}m_Age = new int(*p.m_Age);}
public:int *m_Age;
};void test01()
{Person p1(18);Person p2(20);p2 = p1; //赋值操作,类自带。值拷贝cout << "p1的年龄为:" << *p1.m_Age << endl;//输出18cout << "p2的年龄为:" << *p2.m_Age << endl;//输出18
}
以上代码对于p2 = p1这种一个等号的赋值操作已经可以了。但是C++的赋值操作可以连等。连等p3=p2=p1逻辑上就是p2先等于p1然后p3再等于p2,而上述代码重载函数返回值为void,也就意味着p3=void,所以只需要将重载函数的返回值改为能返回p2的即可
#include<iostream>
using namespace std;class Person
{
public:Person(int age){m_Age = new int(age);}~Person(){if(m_Age!=NULL){delete m_Age;m_Age = NULL;}}//重载赋值运算符Person& operator+(Person&p){//编译器提供的浅拷贝就是m_Age = p.m_Age;//应该先判断是否有属性在堆区,如果有就先释放干净,然后再深拷贝if(m_Age != NULL){delete m_Age;m_Age = NULL;}m_Age = new int(*p.m_Age);return *this;}
public:int *m_Age;
};void test01()
{Person p1(18);Person p2(20);Person p3(30);p3 = p2 = p1; //赋值操作,类自带。值拷贝cout << "p1的年龄为:" << *p1.m_Age << endl;//输出18cout << "p2的年龄为:" << *p2.m_Age << endl;//输出18cout << "p2的年龄为:" << *p3.m_Age << endl;//输出18
}
(5)关系运算符重载
重载关系运算符,可以让两个自定义数据类型对象进行对比操作。以下只展示==号,!=类似
#include<iostream>
using namespace std;class Person
{
public:Person(int age, string name){m_Age = age;m_name = name;}//重载==号bool operator==(Person&p){if(this->m_name == p.m_Name && this->m_Age == p.m_Age){return true;}else{return false;}}
public:int m_Age;string m_name;
};void test01()
{Person p1("Tom", 18);Person p2("Tom", 18);if(p1 == p2) cout << "p1和p2相等" << endl;
}
(6)函数调用运算符重载
函数调用运算符()也可以重载
由于重载后使用的方式非常像函数的调用,因此被称为仿函数。仿函数没有固定写法,非常灵活
#include<iostream>
#include<string>
using namespace std;class MyPrint
{
public://重载函数调用运算符void operator()(string test){cout << test << endl;}
};class MyAdd
{
public:int operator()(int num1, int num2){return num1 + num2;}
};void test01()
{MyPrint myPrint;myPrint("hello world");
}void test02()
{MyAdd myadd;int ret = myadd(100, 100);cout << "ret = " << ret << endl;//匿名函数对象cout << MyAdd()(100, 100) << endl;//MyAdd()创建一个匿名对象,当前行结束后立即释放}
6,继承
继承是面向对象的三大特性之一。继承好处就是可以减少重复代码
继承一方称为子类,也叫派生类。被继承一方叫做父类,也成为基类
(1)基本语法
#include<iostream>
#include<string>
using namespace std;class BasePage
{
public:void head(){cout << "BasePage" << endl;}
};class Java : public BasePage //这样Java就继承了BasePage中的属性行为
{
public:void content(){cout << "Java" << endl;}}void test01()
{Java ja;ja.head();ja.content;//输出BasePage 和 Java。说明Java继承了BasePage
}
(2)继承方式
继承方式一般有3种 :公共继承 保护继承 私有继承
继承语法: class 子类 : 继承方式 父类
对于父类的private权限,无论哪种继承方式都不可以访问。保护继承使父类中除私有属性外的其他属性到子类中都变为了保护权限。私有继承使父类中除私有属性外的其他属性到子类中都变为了私有权限。
(2)继承中的对象模型
子类可以继承父类中所有共性的内容。那么从父类继承过来的成员,哪些属于子类对象中?
#include<iostream>
#include<string>
using namespace std;class Base
{
public:int m_A;
protected:int m_B;
private:int m_C;//私有成员只是隐藏了,但是还是会继承下去
};class Son: public Base
{
public:int m_D;
};void test01()
{cout << sizeof(Son) << endl;//输出结果为16//Son将父类中的所有非静态成员属性都继承下来。//父类中私有成员属性 是被编译器隐藏了。因此访问不到,但是确实被继承下去了
}
(3)继承中的构造和析构顺序
子类继承父类后,当创建子类对象时也会创建父类对象。
我们在类对象作为类成员一节说到的:当其他类对象作为本类成员构造时候先构造其他类对象,再构造自身,析构的顺序与构造相反
子类和父类之间的函数关系是一样的:先构造父类,再构造子类,西析构的顺序与构造相反。
(4)同名成员处理(属性和函数)
当子类和父类出现同名成员时,如何通过子类对象访问到子类或者父类中同名的数据?
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
1,同名成员属性
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}int m_A;
};void test01()
{Son s;cout << s.m_A << endl;
}
以上的输出结果是200。也就是重名成员直接访问也就是访问的子类的成员。要访问父类成员,如下。在成员名前加上父类作用域Base::
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}int m_A;
};void test01()
{Son s;cout << s.Base::m_A << endl; //输出为100
}
2,同名成员函数
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;}void func(){cout << "Base - func()调用" << endl;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}int m_A;
};void test01()
{Son s;s.func();
}
如上所示调用会输出Base - func()调用。因为子类和父类的名为func()的函数只有一个。
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;}void func(){cout << "Base - func()调用" << endl;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}void func(){cout << "Son - func()调用" << endl;}int m_A;
};void test01()
{Son s;s.func();
}
如上所示输出 Son - func()调用 也就是说直接调用是子类中的同名函数
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;}void func(){cout << "Base - func()调用" << endl;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}void func(){cout << "Son - func()调用" << endl;}int m_A;
};void test01()
{Son s;s.Base::func();
}
如上所示加作用域即可输出 Base - func()调用
有一个需要注意的点:如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏父类中所有同名成员函数。也就是说父类中尽管用函数重载的方式区分了(比如说加一个参数int a),但是子类中不认识这个函数,调用s.func(100)会出错。如下代码演示
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;} void func(){cout << "Base - func()调用" << endl;}void func(int a){cout << "Base - func(int a)调用" << endl;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}void func(){cout << "Son - func()调用" << endl;}int m_A;
};void test01()
{Son s;s.func(100);
}
以上代码s.func(100);会报错,子类不认识这个函数。如果想访问到父类中被隐藏的同名成员函数,需要加作用域。如下代码正确
#include<iostream>
#include<string>
using namespace std;class Base
{
public:Base(){m_A = 100;} void func(){cout << "Base - func()调用" << endl;}void func(int a){cout << "Base - func(int a)调用" << endl;}int m_A;
};class Son: public Base
{
public:Son(){m_A = 200;}void func(){cout << "Son - func()调用" << endl;}int m_A;
};void test01()
{Son s;s.Base::func(100);
}
(5)同名静态成员处理
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
#include<iostream>
#include<string>
using namespace std;class Base
{
public:static int m_A;
};class Son: public Base
{
public:static int m_A;
};int Base::m_A = 100;
int Son::m_A = 200;void test01()
{Son s;s.Base::m_A;
}
除此之外静态成员属性和函数还可以通过类名的方式访问。如,Son::m_A Base::m_A或者是Son::Base::m_A 。 而非静态成员不可以
(6)多继承语法
C++允许一个类继承多个类
语法:class 子类:继承方式 父类1, 继承方式 父类2.....
多继承可能会引发父类中有同名成员出现,需要加作用域区分。C++实际开发中不建议使用多继承
#include<iostream>
#include<string>
using namespace std;class Base1
{
public:Base1{m_A = 100;}int m_A;
};class Base2
{
public:Base2{m_B = 200;}int m_B;
}; class Son:pubilc Base1, public Base2
{
public:Son(){m_C = 300;m_D = 400;}int m_C;int m_D;
};void test01()
{}
当父类中出现了同名成员,需要加作用域加以区分
#include<iostream>
#include<string>
using namespace std;class Base1
{
public:Base1{m_A = 100;}int m_A;
};class Base2
{
public:Base2{m_A = 200;}int m_A;
}; class Son:pubilc Base1, public Base2
{
public:Son(){m_C = 300;m_D = 400;}int m_C;int m_D;
};void test01()
{Son s;cout << s.Base1::m_A << endl;
}
(7)菱形继承
菱形继承概念:两个派生类继承同一个基类,同时又有某个类同时继承两个派生类,这种继承被称为菱形继承,或者钻石继承。
当两个派生类继承了同一个基类时,当又继承了这两个派生类的子类调用某个属性时,就会产生二义性,就是对于两个父类都有同名成员,这可以通过上一节说的作用域来解决,但是实际上继承了同一个属性的两份数据,而我们只需要一份即可
#include<iostream>
#include<string>
using namespace std;class Animal
{
public:int m_Age;
};
//利用虚继承可以解决继承同一属性两份而导致的资源浪费问题
//Animal类称为虚基类
class Sheep : virtual public Animal{};
class Tuo : virtual public Animal{};
class SheepTuo : public Sheep, public Tuo{};void test01()
{SheepTuo st;st.Sheep::m_Age = 18;st.Tuo::m_Age = 28;//最终m_Age为28,虚继承后只有一份m_Age,共享使用st.m_Age = 38;//虚继承之后只有一份数据,可以直接用st.m_Age访问
}
7,多态
(1)多态基本语法
多态是C++面向对象三大特性之一
多态分为两类:静态多态和动态多态。函数重载(复用函数名)和运算符重载属于静态多态,之前已经学过了。动态多态,由派生类和虚函数实现运行时多态。
静态多态和动态多态区别在于函数地址是早绑定还是晚绑定。静态多态在编译阶段确定函数地址,称为早绑定;动态多态在运行阶段确定函数地址,称为晚绑定
#include<iostream>
#include<string>
using namespace std;class Animal
{
public:void speak(){cout << "speak" << endl;}
};class Cat : public Animal
{
public:void speak(){cout << "cat speak" << endl;}
};void doSpeak(Animal &animal)
{animal.speak();
}
void test01()
{Cat cat;doSpeak(cat);
}
以上代码没有语法错误。但是在test01()函数中创建的是Cat数据类型,而doSpeak()函数接收的是Animal的数据类型,相当于父类的引用在指向接收子类的一个对象,即Animal& animal = cat;。这是因为在C++中允许父子之间的类型转换,不需要强制类型转换,所以父类的指针可以直接指向子类对象。
但是最终的执行结果是speak,也就是执行了父类中的speak函数。这是因为animal.speak();这个函数的地址是早绑定,在编译阶段就确定了函数地址,函数接收的是animal对象,不管传入什么对象,都是调用的Animal类中的speak()函数
如果想要输出Cat类中的speak()函数,就要利用动态多态,让地址晚绑定。在父类中函数前加virtual即可实现晚绑定
#include<iostream>
#include<string>
using namespace std;class Animal
{
public://虚函数virtual void speak(){cout << "speak" << endl;}
};class Cat : public Animal
{
public:void speak(){cout << "cat speak" << endl;}
};void doSpeak(Animal &animal)
{animal.speak();
}
void test01()
{Cat cat;doSpeak(cat);
}
动态多态的条件:
- 要有继承关系
- 子类要重写父类的(函数返回值 函数名 参数列表要完全相同才叫函数重写。virtual可写可不写)
动态多态的使用:父类的指针或引用要指向子类对象
(2)多态原理剖析
如果上述Animal类中speak()函数不加virtual,那Animal就是一个空类,占用内存为1,加了virtual之后,Animal类占用内存就变成了4。这4个字节的内存来自指针,这个指针叫做vfptr(virual function pointer,即虚函数(表)指针),指向虚函数表(叫做vftable,即virtual function table),表内部记录着一个虚函数的地址(&Animal::speak())
子类Cat如果内部没有发生函数重写的情况,就是一个继承,将父类中所有内容都拿到子类中一份。这样子类Cat也继承了一份指针vfptr,并且指向了子类的虚函数表,其内部记录着也是一个虚函数的地址(&Animal::speak())。
但是如果子类发生了重写,子类会将虚函数表的内容做一个覆盖操作,替换成子类的虚函数地址(&Cat::speak())
当父类的指针或引用指向父类时,会发生多态。Animal&animal = cat;这时animal对象调用speak()函数时会从Cat的虚函数表中去找这个函数
(3)纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数,当类中有了纯虚函数,这个类也称为抽象类
语法: virtual 返回值类型 函数名(参数列表) = 0;
抽象类无法实例化对象。子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include<iostream>
using namespace std;classs Base
{
public:virtual void func() = 0;//纯虚函数
};void test01()
{Base b;//错误,无法实例化对象!new Base;//错误,栈区也不能实例化!
}
#include<iostream>
using namespace std;classs Base
{
public:virtual void func() = 0;//纯虚函数
};class Son : public Base
{
public:void func{}
};void test01()
{Son s;//子类必须重写父类纯虚函数,否则无法实例化
}
(4)虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类中的析构代码,这就需要将父类中的虚构函数改为虚析构或者纯虚析构
如果代码中有了纯虚析构,该类属于抽象类,无法实例化对象
#include<iostream>
using namespace std;classs Animal
{
public:Animal(){cout << "Animal构造函数调用" << endl;}~Animal(){cout << "Animal析构函数调用" << endl;}virtual void speak() = 0;
};class Cat : public Animal
{
public:Cat(string name){ cout << "Cat构造函数调用" << endl;m_Name = new string(name);}void speak{cout << *m_Name << "Cat speak" << endl;}string* m_Name;~Cat(){if(m_Name != NULL){cout << "Cat析构函数调用" << endl;delete m_Name;m_Name = NULL;}}
};void test01()
{Animal*animal = new Cat("Tom");animal -> speak();delete animal;
}
以上程序运行后没有出现问题。输出结果如下。但是缺少了Cat也就是子类的析构函数调用,也就是堆区内存无法释放(我们使用的是父类的指针指向子类的对象,而delete父类对象时不会走子类的析构函数)
将父类的析构改为虚析构即可解决上述问题
#include<iostream>
using namespace std;classs Animal
{
public:Animal(){cout << "Animal构造函数调用" << endl;}virtual ~Animal(){cout << "Animal析构函数调用" << endl;}virtual void speak() = 0;
};class Cat : public Animal
{
public:Cat(string name){ cout << "Cat构造函数调用" << endl;m_Name = new string(name);}void speak{cout << *m_Name << "Cat speak" << endl;}string* m_Name;~Cat(){if(m_Name != NULL){cout << "Cat析构函数调用" << endl;delete m_Name;m_Name = NULL;}}
};void test01()
{Animal*animal = new Cat("Tom");animal -> speak();delete animal;
}
以上代码执行结果如下,可以看到子类和父类的析构函数都运行了
纯虚析构就是 virtual ~Animal = 0;
但是只将上述代码的虚析构改为virtual ~Animal = 0;会报错。这是因为纯虚析构必须要有函数的具体实现,它的具体函数实现的书写方式如下。在类外实现。
#include<iostream>
using namespace std;classs Animal
{
public:Animal(){cout << "Animal构造函数调用" << endl;}virtual ~Animal() = 0;virtual void speak() = 0;
};
Animal::~Animal()
{//函数内容
}class Cat : public Animal
{
public:Cat(string name){ cout << "Cat构造函数调用" << endl;m_Name = new string(name);}void speak{cout << *m_Name << "Cat speak" << endl;}string* m_Name;~Cat(){if(m_Name != NULL){cout << "Cat析构函数调用" << endl;delete m_Name;m_Name = NULL;}}
};void test01()
{Animal*animal = new Cat("Tom");animal -> speak();delete animal;
}
十三,文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束就会被释放。通过文件可以将数据持久化。
C++对文件操作需要包含头文件<fsteam>文件流
文件类型分为两种,一种为文本文件(文件以文本的ASCII码形式储存在计算机中);一种是二进制文件(文件以文本的二进制形式存储在计算机中)
操作文件三大类:ofstream(写操作),ifstream(读操作),fstream(读写操作)
1,写文本文件
写文件步骤:
- 包含头文件:#include<fstream>
- 创建流对象:ofstream ofs;
- 打开文件:ofs.open(“文件路径”,打开方式);
- 写数据:ofs << "写入的数据"
- 关闭文件:ofs.close();
文件打开方式如下:
文件打开方式可以配合使用,利用|操作符。例如用二进制的方式写文件ios::binary | ios::out
#include<iostream>
using namespace std;
#include<fstream>void test01()
{ofstream ofs;//用fstream类也可以ofs.open("test.txt", ios::out);//未指定路径,则文件创建在c++文件同级文件下ofs << "姓名" << endl;ofs << "年龄" << endl;ofs.close();
}
2,读文本文件
- 包含头文件:#include<fstream>
- 创建流对象:ifstream ifs;
- 打开文件并判断是否打开成功:ifs.open(“文件路径”,打开方式); ifs.isopen()判断是否打开成功,成功则返回true,否则为false
- 写数据:四种方式读取
- 关闭文件:ifs.close();
#include<iostream>
using namespace std;
#include<fstream>
#include<string>void test01()
{ifstream ifs;ifs.open("test.txt", ios::in);if(!ifs.isopen()){cout << "文件打开失败" << endl;return;}//读数据//第一种char buf[1024] = { 0 };while(ifs >> buf){cout << buf << endl;}//第二种char buf[1024] = { 0 };
//getline是获取一行的函数,第一个参数是将读出来的数据放到哪,第二个参数是准备了多大空间来存储while(ifs.getline(buf, sizeof(buf))){cout << buf << endl;}//第三种string buf;while(getline(ifs, buf)){cout << buf << endl;}//第四种
//将文件中的所有数据一个一个字符读出来。get()函数可以每一次读一个字符char c;while((c = ifs.get()) != EOF){cout << c;}ifs.close();
}
3,写二进制文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char*buffer, int len);//字符指针buffer指要写入的数据的地址,len是读写的字节数
二进制文件操作不只是可以操作内置的数据类型,还可以操作自定义数据类型
#include<iostream>
using namespace std;
#include<fstream>class Person
{
public:car m_Name[64];int m_Age;
};void test01()
{ofstream ofs;ofs.open("person.txt", ios::out | ios::binary);Person p = {"张三", 18};ofs.write((const char*)&p, sizeof(Person));ofs.close();
}
二进制方式写的文件打开后会出现乱码,这很正常,只要读的时候没事就可以
4,读二进制文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char*buffer, int len);//字符指针buffer指读取的数据要存放到的地址,len是读写的字节数
#include<iostream>
using namespace std;
#include<fstream>class Person
{
public:car m_Name[64];int m_Age;
};void test01()
{ifstream ifs;ifs.open("person.txt", ios::in | ios::binary);if(!ifs.open()){cout << "文件打开失败" << endl;return;} Person p;ofs.read((char*)&p, sizeof(Person));cout << "姓名:" << p.m_Name << "年龄:" << p.m_Age << endl;ofs.close();
}
第二阶段
十四,模板
从此章开始进入C++提高编程阶段。本阶段主要针对C++泛型编程和STL技术做讲解
模板就是建立通用的模具,大大提高复用性
C++提供两种模板机制:函数模板和类模板
1,函数模板
函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不指定,用一个虚拟的类型来代表
(1)语法
template<typename T> 函数声明或定义
template声明创建模板。typename表明其后面的符号是一种数据类型,可以用class代替。T为通用数据类型名称可以替换,通常为大写字母
#include<iostream>
using namespace std;//函数模板.创建交换数据函数
template<typename T> //声明一个模板,告诉编译器后年函数里的T是一个通用数据类型
void mySwap(T &a, T &b)
{T temp = a;a = b;b = temp;
}void test01()
{int a = 10;int b = 20;//两种方式使用函数模板//1,自动类型推导mySwap(a, b);//编译器自己推导a和b是什么类型//2,显示指定类型mySwap<int>(a, b);
}
(2)注意事项
自动类型推导,必须推导出一致的数据类型T才可以使用;模板必须要确定出T的数据类型才可以使用
#include<iostream>
using namespace std;template<class T> //typename可以替换为class实际上也没什么区别,效果一样
void mySwap(T &a, T &b)
{T temp = a;a = b;b = temp;
}void test01()
{int a = 10;int b = 20;char c = 'c';mySwap(a, c);//错误,推导出了不一致的T类型
}
#include<iostream>
using namespace std;template<class T> //typename可以替换为class实际上也没什么区别,效果一样
void func()
{cout << "aaa" << endl;
}void test01()
{func();//错误,未指定T的数据类型func<int>();//正确。
}
(3)普通函数和函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型转换不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
#include<iostream>
using namespace std;int myAdd01(int a, int b)
{return a+b;
}
void test01()
{int a = 10;int b = 20;char c = 'c';cout << myAdd01(a, c) << endl; //可以自动将c转换为整型,即隐式类型转换
}
#include<iostream>
using namespace std;template<class T>
T myAdd02(T a, T b)
{return a + b;
}void test01()
{int a = 10;int b = 20;char c = 'c';cout << myAdd01(a, c) << endl; //错误,自动推导不会类型转换cout << myAdd02<int>(a, c);//正确,可以进行隐式类型转换
}
(4)普通函数和模板函数的调用规则
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
#include<iostream>
using namespace std;template<class T>
void myPrint(T a, T b)
{cout << "函数模板" << endl;
}template<class T>
void myPrint(T a, T b, T c)
{cout << "函数模板重载" << endl;
}void myPrint(int a, int b)
{cout << "普通函数" << endl;
}
void test01()
{int a = 10;int b = 20;myPrint(a, b); //如果函数模板和普通函数都可以调用,优先调用普通函数myPrint<>(a, b); //通过空模板的参数列表强制调用函数模板myPrint(a, b, 100); //函数模板也可以发生函数重载char c1 = 'a';char c2 = 'b';myPrint(c1, c2);//输出为 函数模板。这是因为调用普通函数需要将char转换为int,而调用模板可以推 //导为char类型//如果函数模板可以产生更好的匹配,优先调用函数模板
}
建议:如果提供了函数模板就不要提供普通函数,否则容易出现二义性
(5)模板的局限性
模板的通用性不是万能的
在下述代码提供的赋值操作,如果传入的a和b是一个数组就无法实现了(数组之间无法直接赋值)
template<class T>
void f(T a, T b)
{a = b;
}
在下述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行
template<class T>
void f(T a, T b)
{if(a > b){......}
}
为了解决上述问题,C++提供了模板的重载,可以为这些特定的类型提供具体化的模板
#include<iostream>
#include<string>
using namespace std;class Person
{
public:Person(string name, int age){this -> m_Name = name;this -> m_Age = age;}string m_Name;int m_Age;
};template<class T>
bool myCompare(T &a, T &b)
{if(a == b) return true;else return false;
}
//利用具体化的Person版本实现代码,具体化会优先调用
template<> bool myCompare(Person &a, Person &b)
{if(p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age){return true;}else{return false;}
}void test01()
{int a = 10;int b =20;bool ret = myCompare(a, b);
}
void test02()
{Person p1("Tom", 10);Person p2("Tom", 10);bool ret = myCompare(a, b);//错误,无法比较自定义类型
}
利用具体化的模板,可以解决自定义类型的通用化
学习模板不是为了写模板,而是在STL能够运用系统提供的模板
2,类模板
建立一个通用类,类中的成员数据类型可以不具体指定,用一个虚拟的类型来代表
(1)语法
template<typename T> 类
#include<iostream>
#include<string>
using namespace std;template<class NameType, class AgeType>
class Person
{
public:Person(NameType name, AgeType age){this -> m_Name = name;this -> m_Age = age;}NameType m_Name;AgeType m_Age;
};void test01()
{Person<string, int> p1("Tom", 10);
}
(2)类模板与函数模板的区别
类模板没有自动类型推导的使用方式
类模板在模板参数列表中可以有默认参数
#include<iostream>
#include<string>
using namespace std;template<class NameType, class AgeType>
class Person
{
public:Person(NameType name, AgeType age){this -> m_Name = name;this -> m_Age = age;}NameType m_Name;AgeType m_Age;
};//类模板没有自动类型推导使用方式
void test01()
{Person p1("Tom", 10);//错误,不能自动推导Person<string, int> p1("Tom", 10);//正确,只能用显示指定类型
}
#include<iostream>
#include<string>
using namespace std;template<class NameType, class AgeType = int>
class Person
{
public:Person(NameType name, AgeType age){this -> m_Name = name;this -> m_Age = age;}NameType m_Name;AgeType m_Age;
};void test01()
{Person<string> p1("Tom", 10);//类模板在模板列表中可以有默认参数
}
(3)类模板中成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机有区别:普通类中的成员函数一开始就可以创建;类模板中的成员函数在调用时才会创建
#include<iostream>
#include<string>
using namespace std;class Person1
{
public:void showPerson1(){cout << "Person1 show" << endl;}
};class Person2
{
public:void showPerson2(){cout << "Person2 show" << endl;}
};template<class T>
class MyClass
{
public:T obj;//类模板中的成员函数void func1(){obj.showPerson1();}void func2(){obj.showPerson2();}
};void test01()
{}
上述代码可以编译通过。这是因为类模板里的两个函数只要不调用,编译器就不会去创建。因为编译器并不知道obj是什么数据类型
#include<iostream>
#include<string>
using namespace std;class Person1
{
public:void showPerson1(){cout << "Person1 show" << endl;}
};class Person2
{
public:void showPerson2(){cout << "Person2 show" << endl;}
};template<class T>
class MyClass
{
public:T obj;T obj2;//类模板中的成员函数void func1(){obj.showPerson1();}void func2(){obj2.showPerson2();}
};void test01()
{MyClass<Person1>m; MyClass<Person2>m2; m.func1(); //调用后才会创建函数m2.func2();
}
(4)类模板对象做函数参数
类模板实例化出的对象,向函数传参的方式
三种传入方式:
- 指定传入的类型 --直接显示对象的数据类型(使用最广泛)
- 参数模板化 --将对象中的参数变为模板进行传递
- 整个类模板化 --将这个对象类型模板化进行传递
#include<iostream>
#include<string>
using namespace std;template<class T1, class T2>
class Person
{
public:Person(T1 name, T2 age){this -> m_Name = name;this -> m_Age = age;}void showPerson(){cout << this->m_Name <<this->m_Age << endl;}T1 m_Name;T2 m_Age;
};//指定传入类型
void printPerson1(Person<string, int>&p)
{p.showPerson();
}//参数模板化
template<class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{p.showPerson();cout << "T1的类型为" << typid(T1).name() << endl;//通过typid()可以查看T1的类型
}//整个模板化
template<class T>
void printPerson3(T &p)
{p.showPerson();cout << "T1的类型为" << typid(T1).name() << endl;
}void test01()
{Person<string, int>p("Tom1", 18);printPerson1(p);Person<string, int>p("Tom2", 28 );printPerson2(p);Person<string, int>p("Tom3", 38 );printPerson3(p);
}
(5)类模板与继承
- 当子类继承的父类是一个类模板时,子类在声明的时候要指出父类中T的类型,如果不指定编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需要变为类模板
#include<iostream>
#include<string>
using namespace std;template<class T>
class Base
{
public:T m;
};class Son:public Base<int>//必须指出父类中T的类型
{
};//如果想灵活指定父类中T的类型,那么子类也需要变为类模板
template<class T1, class T2>
class Son2:public Base<T2>
{T1 obj;
};void test01()
{Son2<int, char>S2;
}
(6)类模板成员函数的类外实现
#include<iostream>
#include<string>
using namespace std;template<class T1, class T2>
class Person
{
public:Person(T1 name, T2 age);void showPerson();T1 m_Name;T2 m_Age;
};//构造函数类外实现
template<class T1, class T2>
Person<T1,T2>::Person(T1 name, T2 age)
{this->m_Name = name;this->m_Age = age;
}
//成员函数类外实现
template<class T1, class T2>
void Person<T1,T2>::showPerson()
{cout << this->m_Name << this->m_Age << endl;
}void test01()
{}
(7)类模板函数分文件编写
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决方法:
- 直接包含.cpp源文件
- 将声明和实现写到同一个文件中,并更改后缀为.hpp,hpp是约定的名称,并不是强制
Person.h文件
#pragma once//防止头文件重复包含
#include<iostream>
#include<string>
using namespace std;template<class T1, class T2>
class Person
{
public:Person(T1 name, T2 age);void showPerson();T1 m_Name;T2 m_Age;
};
Person.cpp文件
#include"Person.h"
#include<string>
using namespace std;template<class T1, class T2>
Person<T1,T2>::Person(T1 name, T2 age)
{this->m_Name = name;this->m_Age = age;
}template<class T1, class T2>
void Person<T1,T2>::showPerson()
{cout << this->m_Name << this->m_Age << endl;
}
main.cpp
#include<iostream>
using namespace std;#include"person.h"void test01()
{Person<string, int>p("Tom" 18);p.showPerson();
}int main()
{test01();system("pause");return 0;
}
运行上述main.cpp文件后出错
解决方法1是在main.c文件里引用"person.cpp"而不是"person.h"。这是因为类模板里的成员函数在调用类模板时不会创建,只有在调用此函数时才会创建,所以在包含.h文件时,相当于将.h内的内容给了编译器,但是编译器并不会生成成员函数,并且.cpp文件里的内容编译器从来没有见到过。
解决方法2是将.h和.cpp中的内容写到一起,然后将后缀名改为.hpp文件.在main.cpp文件里直接写#include"Person.hpp"即可
Person.hpp
#pragma once//防止头文件重复包含
#include<iostream>
#include<string>
using namespace std;template<class T1, class T2>
class Person
{
public:Person(T1 name, T2 age);void showPerson();T1 m_Name;T2 m_Age;
};template<class T1, class T2>
Person<T1,T2>::Person(T1 name, T2 age)
{this->m_Name = name;this->m_Age = age;
}template<class T1, class T2>
void Person<T1,T2>::showPerson()
{cout << this->m_Name << this->m_Age << endl;
}
(8) 类模板与友元
掌握类模板配合友元的类内和类外实现
全局函数类内实现-直接在类内声明友元即可
全局函数类外实现-需要提前让编译器知道全局函数的存在
#include<iostream>
#include<string>
using namespace std;//提前让编译器知道Person类的存在
template<class T1, class T2>
class Person;template<class T1, class T2>
void printPerson2(Person<T1, T2>p)
{cout << "类外实现" << p.m_Name << p.m_Age << endl;
}template<class T1, class T2>
class Person
{//全局函数类内实现friend void printPerson(Person<T1, T2>p){cout << p.m_Name << p.m_Age << endl;}//全局函数类外实现//加空模板参数列表//如果全局函数是类外实现,需要让编译器提前知道这个函数的存在//此函数本质发生了变化,变为了函数模板,需要让编译器提前知道这个模板的存在//所以将此函数的实现放到了最开始friend void printPerson2<>(Person<T1, T2>p);
public:Person(T1 name, T2 age){this->m_Name = name;this->m_Age = age;}void showPerson();private:T1 m_Name;T2 m_Age;
};
void test01()
{Person<string, int>p("Tom", 20);printPerson(p);Person<string, int>p("Tom2", 10);printPerson2(p);
}