一、工具安装
Microsoft Visual Studio(简称 VS)是美国 微软公司 的开发工具 包系列产品。VS 是一个基本 完整 的开发工具集,它包括了整个 软件生命周期 中所需要的大部分工具,如 UML 工具、代码 管控 工具、集成开发环境 (IDE) 等等。
社区免费版:https://visualstudio.microsoft.com/zh-hans/downloads/
【编辑器】:纯用来写代码的,如 vim
【编译器】:将源程序转化为二进制形式的目标程序的工具,如 GCC
【IDE】:集成了编辑器、编译器以及链接器等众多功能的一个集成开发环境。
二、基础入门
2.1 认识 C++
2.1.1 注释
单行注释:
// 注释信息
多行注释:
/*
多行注释 1
多行注释 2
*/
2.1.2 变量与常量
变量:给一段内存起名,方便我们管理内存。
// 变量创建 数据类型 变量名 = 变量初始值;
int a = 10;
常量:用于记录程序中不可更改的数据。
C++ 中定义常量的两种方法:
- 宏常量:
#define 常量名 常量值
。通常在文件开头定义。 - const 修饰的常量:
const 数据类型 常量名 = 常量值
#include<iostream>
using namespace std;
#define Day 7 // 宏常量
int main(){
const int a = 10; //const 修饰的常量
cout<<Day<<endl;
cout<<a<<endl;
system("pause")
return 0;
}
2.1.3 关键字
作用:关键字是 C++ 中保留的单词(标识符),在 C++ 中拥有固定的,预设的含义。因此在定义变量 / 常量的时候,禁止使用关键字。
关键字 | 关键字 | 关键字 | 关键字 | 关键字 |
---|---|---|---|---|
asm | do | if | return | typedef |
auto | double | inline | short | typeid |
bool | dynamic | int | signed | typename |
break | else | long | sizeof | union |
case | enum | mutable | static | unsigned |
catch | explict | namespace | static_cast | using |
char | export | new | struct | virtual |
class | extern | operator | switch | void |
const | false | private | template | volatile |
const_cast | float | protected | this | wchar_t |
continue | for | public | throw | while |
default | friend | register | true | |
delete | goto | reinterpret_cast | try |
2.1.4 标识符命名规则
作用:C++ 规定给标识符(变量,常量)命名时,有一套自己的规则
- 标识符不能是关键字
- 标识符只能由字母,数字,下划线组成
- 第一个字符必须为字母或者下划线组成
- 标识符中字母区分大小写
给标识符命名时,尽量做到见名知意,方便阅读。
2.1.5 main 函数
在 C++ 中,main 函数为必须的,C++ 程序自动从 main 函数开始执行。
2.2 数据类型
C++ 在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存。
可以通过 typeid(变量名).name()
查看数据类型。
C++ 为程序员提供了种类丰富的内置数据类型和用户自定义的数据类型。下表列出了七种基本的 内置C++ 数据类型:
类型 | 关键字 | 可加修饰符 |
---|---|---|
整型 | int | signed、unsigned、short、long |
单浮点型 | float | / |
双浮点型 | double | long |
字符型 | char | signed、unsigned |
布尔型 | bool | / |
无类型 | void | / |
宽字符型 | wchar_t | / |
其实 wchar_t 是这样来的:
typedef short int wchar_t;
C++ 允许使用速记符号来声明 无符号短整数 或无符号长整数。可以不写 int,只写单词 unsigned、short 或 long,int 是隐含的。
- cout 输出时,默认只输出六位有效数字。可使用 cout.precision(10); 更改为 10 位有效数字。
2.2.1 整型
作用:用于表示整数类型的数据。
整型类型:
数据类型 | 占用空间 | 取值范围 |
---|---|---|
short(短整型) | 2 byte(字节) | -2^15 – 2^15-1 (-32786–32767) |
int(整型) | 4 byte | -2^31 – 2^31-1 |
long(长整型) | 4 byte (window/linux x32); 8 byte(linux x64) | -2^31 – 2^31-1 |
long long (长长整型) | 8 byte | -2^63 – 2^63-1 |
signed int(有符号整型) | 4 byte | -2^31 – 2^31-1 |
unsigned int(无符号整型) | 4 byte | 0 – 2^32-1 |
unsigned short(无符号短整型) | 2 byte | 0~65535 |
数据类型存在的意义:给变量分配合适的内存空间,避免造成资源浪费。1byte 字节 =8bit 比特,二进制数系统中,每个 0 或 1 就是一个位(bit)
MB 与 byte 换算:
1MB = 1048576byte = 262144 int 类型数据
// 一个内存溢出造成数据错误的例子:
#include<iostream>
using namespace std;
#define Day 7
int main() {
short a = 33000;
cout << a << endl;
system("pause");
return 0;
}
// 输出 a 的结果:-32536
利用 sizeof 关键字可以统计数据类型所占的内存空间大小
语法:sizeof(数据类型) 或者 sizeof(变量名字)
注意:将一个负数强制转成无符号数,并不是取绝对值的关系:
int main()
{
int a = -1;
cout << (unsigned)a << endl; // 输出 4294967295,即 2^32-1
int b = -10;
cout << (unsigned)b << endl; // 输出 4294967286,即 2^32-10
}
int 类型数据取值范围为什么是 -2^31 – 2^31-1?
举个例子:
2^(4*8-1)=2^31
因为 0 的源码是:
00000000 00000000 00000000 00000000 占了一个位置,所以正整数范围是0-2^31-1
一共是 2^31 个数。
题外话:
在计算机中,负数的二进制是用其源码的补码储存的。正数是用其源码直接存储。
补码:反码加 1 称为补码。
反码:将二进制源码按位取反。
111001 源码
000110 反码
000111 补码
2.2.2 浮点型
作用:用于表示小数。
浮点型分为两种:
- 单精度 float
- 双精度 double
数据类型 | 占用空间 | 有效数字范围 |
---|---|---|
float | 4 byte | 7 位有效数字 |
double | 8 byte | 15-16 位有效数字 |
long double | 16 byte | 18-19 位有效数字 |
//...
int main() {
float a = 3.14f;
double b = 3.141592654;
cout << a << endl;
cout << b << endl;
// 科学计数法
float c = 2e5; // 2*10^5
system("pause");
return 0;
}
/* 输出
3.14
3.14159
*/
默认情况下,输出一个小数,最多只能输出 6 位有效数字。
2.2.3 字符型
作用:字符型变量用于显示单个字符
语法:char ch = 'a'
在显示字符型变量时,用单引号括起来,不要用双引号。
单引号内只能有一个字符,不可以是字符串。
C 和 C++ 中,字符型变量只占用 1 个字节
数据类型 | 占用空间 | 取值范围 |
---|---|---|
char | 1byte | -2^7– 2^7-1 |
unsigned char | 1byte | 0–2^8-1 |
字符型变量并不是把字符本身放到内存中存储,而是将其对应的 ASCII 编码放到存储单元。
//...
int main() {
char ch = 'a';
char ch2 = 'A';
cout << (int)ch << endl; // 输出 97
cout << (int)ch2 << endl; // 输出 65
system("pause");
return 0;
}
// 输出 97 65
1MB = 1048576byte = 1048576 char 类型数据
2.2.4 布尔类型 bool
作用:布尔类型代表真或假
布尔类型只有两个值:
- true — 真(本质是 1)
- false — 假 (本质是 0)
布尔类似只占用 1 个字节。
2.2.5 转义字符
作用:用于表示一些不能显示出来的 ASCII 字符,如一些表示格式的字符。反斜杠 \
现阶段我们常用的转义字符\n \\ \t
。
2.2.6 字符串
作用:用于表示一串字符
两种风格:
C 风格字符串:
char 变量名[] = "abcd"
char ch[] = { 'a','b','c','\0'}; // 必须加结尾符 \0 cout << ch << endl; // 输出 abc,为字符串数组, char ch2[] = "abc"; cout << ch2 << endl; // 输出 abc
C++ 风格字符串:
string 变量名 = “abcd”
如果要使用 C++ 风格字符串,需要加头文件: #include<string>
在 C++ 中,字符串类型本质是一个容器类。
字符串以 \0
字符结尾。
int main()
{
char c[] = { 'a','b','c','d','\0'};
cout << strlen(c) << endl; // 输出 4
cout << sizeof(c) << endl; // 输出 5
string a = "abcd";
cout << sizeof(a) << endl; // 输出 28,字符串 a 本质是一个类
cout << a.length() << endl; // 输出 4
}
为了节省内存和方便,定义字符串的时候,可以采用这种方法:
char ch2[] = "abcd";
cout << sizeof(ch2) << endl; // 输出 5
相比 string a = "abcd";
节省了空间,不过 string 提供很多方法可供使用,后面容器部分会有详细介绍。这里先介绍 C 风格字符串的方法:
C 风格字符串的方法:
函数 | 目的 |
---|---|
strcpy(s1, s2); | 复制字符串 s2 到字符串 s1 |
strcat(s1, s2); | 连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号 |
strlen(s1); | 返回字符串 s1 的长度。 |
strcmp(s1, s2); | 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回值小于 0;如果 s1>s2 则返回值大于 0。 |
strchr(s1, ch); | 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 |
strstr(s1, s2); | 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
2.2.7 进制
进制 | 标识符 | 示例 |
---|---|---|
二进制(0 和 1 组成) | 0b 或者 0B | 0b101=5(十进制) |
八进制(0-7 组成) | 0 | 0555=365(十进制) |
十六进制(0-9,A-F 组成) | 0x 或者 0X | 0x10F=271(十进制) |
转换成十进制的换算方法:
0555 = 5*8^2 + 5*8^1 + 5*8^0 = 365
0x10F = 1*16^2 + 0*16^1 + 15*16^0 = 271
注意:F 代表 15, A 代表 10
总结 :第 i 位数乘以 进制的 (i-1) 次方 + 第i-1 位数乘以 进制的 (i-1-1) 次方 + … + 第 1 位数乘以进制的(1-1) 次方。
利用计算机程序进行进制转换:
#include<iostream>
#include<bitset>
int main()
{
int a = 60;
cout << (bitset<10>)a << endl; // 二进制需要用到 bitset 库, 这里的 10 是输出二进制的位数,输出 10 位二进制数
cout << oct << a << endl; // 八进制输出
cout << hex << a << endl; // 十六进制输出
}
/*
0000111100
74
3c
*/
2.2.8 auto 关键字
C++11 引入了 auto 和 decltype 关键字实现类型推导,通过这两个关键字不仅能方便地获取复杂的类型,而且还能简化书写,提高编码效率。
auto x = 5; // OK: x 是 int 类型
auto pi = new auto(1); // OK: pi 被推导为 int*
const auto *v = &x, u = 6; // OK: v 是 const int* 类型,u 是 const int 类型
static auto y = 0.0; // OK: y 是 double 类型
auto int r; // error: auto 不再表示存储类型指示符
auto s; // error: auto 无法推导出 s 的类型
auto 并不能代表一个实际的类型声明,只是一个类型声明的“占位符”。使用 auto 声明的变量必须马上初始化,以让编译器推断出它的实际类型,并在编译时将 auto 占位符替换为真正的类型。
但是 auto 使用是有限制的:
auto 变量必须在定义时初始化,这类似于 const 关键字。
定义在一个 auto 序列的变量必须始终推导成同一类型。
auto a4 = 10, a5 = 20, a6 = 30; //a4 a5 a6 必须为同一类型
如果初始化表达式是引用,则去除引用语义。
int a = 10; int &b = a; auto c = b;//c 的类型为 int 而非 int&(去除引用) auto &d = b;// 此时 c 的类型才为 int&
如果初始化表达式为 const 或 volatile(或者两者兼有),则除去 const/volatile 语义。
const int a1 = 10; auto b1= a1; //b1 的类型为 int 而非 const int(去除 const) const auto c1 = a1;// 此时 c1 的类型为 const int b1 = 100;// 合法 c1 = 100;// 非法
如果 auto 关键字带上 & 号,则不去除 const 语意。
const int a2 = 10; auto &b2 = a2;// 因为 auto 带上 &,故不去除 const,b2 类型为 const int b2 = 10; // 非法
初始化表达式为数组时,auto 关键字推导类型为指针。
int a3[3] = { 1, 2, 3 }; auto b3 = a3;
若表达式为数组且 auto 带上 &,则推导类型为数组类型。
int a7[3] = { 1, 2, 3 }; auto & b7 = a7;
函数或者模板参数不能被声明为 auto
void func(auto a){} // 错误
时刻要注意 auto 并不是一个真正的类型。
auto 仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如 sizeof 或者 typeid。
2.2.9 数据类型转换
字符串转整型:比如将“123” 转成 123 –> std::stoi(str) = intStr
字符串转单精度:比如将“123.22” 转成 123.22 –> std::stof(str) = floatStr
字符串转双精度:比如将“123.22121” 转成 123.22121 –> std::stod(str) = doubleStr
数值转字符串:比如将 123.11 转成字符串“123.11” –> std::to_string(int) = str
2.3 算术运算
2.3.1 输入与输出
关键字:cin
输入 cout
输出
//...
int main() {
int a, b;
cin >> a >> b;
cout << b << endl;
cout << a << endl;
system("pause");
return 0;
}
计算机怎么知道你终端输入是否完成呢?
cin 以空格为结束符
cin.getline() 以换行符(回车键)为结束,但它不保存换行符,在存储字符串时,它用空字符代替换行符(存储换行符为空字符)
cin.get() 以换行符(回车键)为结束,它读取到换行符的前一个字符,此时换行符仍在输入队列中。
cin.get(name, mSize) // 读取输入保存 mSize 个字符到 name 中
cin.get(diss, mSize) // 第二次调用 cin.get 时首先就看到换行符,故它什么都读取不到就结束了
cin.get()有另一种变体来处理换行符:
cin.get(name, mSize)
cin.get() // 读取下一个字符,包括换行符,可以有返回值
cin.get(diss, mSize)
- cout 默认输出六位有效数字,可使用 cout.precision(10); 更改为 10 位。
2.3.2 算术运算符
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | 10 + 20 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | 10 - 20 将得到 -10 |
* | 把两个操作数相乘 | 10 * 20 将得到 200 |
/ | 除数 | 10 / 20.0 将得到 0.5 |
% | 取模运算符,整除后的余数 | 10 % 20 将得到 10 |
++ | 自增运算符,整数值增加 1 | 10++ 将得到 11 |
– | 自减运算符,整数值减少 1 | 10– 将得到 9 |
2.3.3 关系运算符
运算符 | 描述 |
---|---|
&& | 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 |
2.3.4 位运算符
位运算符作用于位,并逐位执行操作。对两个数的二进制数的每一位进行位运算。
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与操作,按二进制位进行”与”运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; |
二进制(100& 001) 将得到 000 |
| | 按位或运算符,按二进制位进行”或”运算。运算规则: `0 |
0=0; 0 |
^ | 异或运算符,按二进制位进行”异或”运算,相同得 0。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; |
二进制(100 ^ 001) 将得到 101 |
~ | 取反运算符,按二进制位进行”取反”运算。运算规则:~1=0; ~0=1; |
二进制(~000100) 将得到 111011 |
<< | 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补 0)。 | 二进制 000101<< 2 将得到 010100 |
>> | 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 | 二进制 000101 >> 2 将得到 000001 |
位运算的作用:
& 按位与
快速清零
int a=0x0011; a&=0; //a = 0x0011 & 0x0000 = 0x0000
判断奇偶
奇数的二进制末尾一定是 1;偶数的二进制末尾一定是 0。
int a = 2; int b = 3; a = a&1; //a 结果为 0,偶数为 0 b = b&1; //b 结果为 1,奇数为 1
| 按位或
设定指定位的数据
int a=0x0001; int b=0x0010; int c=a|b; //c: 0x0011
^ 按位异或
交换数值
int main() { int a = 32; int b = 34; a = a ^ b; b = b ^ a; a = a ^ b; cout << a << endl; // a:34 cout << b << endl; // b:32 }
<< 和 >> 左移和右移
m << n
等于m*2^n
。int main() { int m = 32; int n = 3; if (m << n == m * pow(2, n)) { cout << "True" << endl; } } // 输出 True
m >> n
等于floor(m/(2^n))
。当 m 为负数,情况就不一样了
int main() { int m = -31; int n = 3; int c = m >> n; cout << c << endl; // 输出 -4 double d = m / pow(2, n); cout << d << endl; // 输出 -3.875 floor(-3.875) = -4 }
2.3.5 赋值运算符
运算符 | 描述 |
---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 |
<<= | 左移且赋值运算符 |
>>= | 右移且赋值运算符 |
&= | 按位与且赋值运算符 |
^= | 按位异或且赋值运算符 |
|= | 按位或且赋值运算符 |
2.3.6 杂项运算符
运算符 | 描述 |
---|---|
sizeof | sizeof 运算符 返回变量所占空间的大小。例如,sizeof(a) 将返回 4,其中 a 是整数 int。 |
Condition ? X : Y | 条件运算符。如果 Condition 为真 ? 则值为 X : 否则值为 Y。 |
, | 逗号运算符 会顺序执行一系列运算。整个逗号表达式的值是以逗号分隔的列表中的最后一个表达式的值。 |
.(点)和 ->(箭头) | 成员运算符 用于引用类、结构和共用体的成员。 |
Cast | 强制转换运算符 把一种数据类型转换为另一种数据类型。例如,int(2.2000) 将返回 2。 |
& | 指针运算符 & 返回变量的地址。例如 &a; 将给出变量的实际地址。 |
* | 指针运算符 * 指向一个变量。例如,*var; 将指向变量 var。 |
2.3.7 类型转换
在 C++ 中,表达式中包含不同的数据类型,C++ 将对其值进行转换。
int main()
{
int a = 10;
double b = 10.0;
if (a == b) {
cout << "true" << endl;
}
else {
cout << "false" << endl;
}
}
上述结果会输出:true
这里的等于判断,只会判断数值是否相等。不判断数据类型。
2.4 程序流结构
C/C++ 支持最基本的三种程序运行结构,顺序结构;选择结构;循环结构。
- 顺序结构:程序按照顺序执行,不发生跳转
- 选择结构:依据条件是否满足,有选择的执行相应功能
- 循环结构:依据条件是否满足,循环多次执行某段代码
2.4.1 选择结构
if 语句
单行格式 if 语句
if (条件){ // 满足条件代码块 };
多行格式 if 语句
if (条件){ // 满足条件代码块 } else{ // 不满足条件代码块 }
多条件的 if 语句
if (条件){ // 满足条件代码块 } else if (条件){ // 不满足条件代码块 } else { // 不满足条件代码块 }
三目运算符
语法:
表达式 1?表达式 2:表达式 3
。解释:如果表达式 1 为真,则执行表达式 2,否则执行表达式 3switch 语句
switch(表达式){ case 结果 1:执行语句 1;break; case 结果 2:执行语句 2;break; case 结果 3:执行语句 3;break; default: 执行语句 3;break; }
2.5 循环结构
作用:满足条件,反复执行某段代码。
2.5.1 while 循环
while(条件){
// 满足条件执行
}
一个猜数字的例子:
int main() {
srand((unsigned int)time(NULL)); // 随机种子,避免每次都是一样的随机数
int a = rand() % 100; // 随机生成 0-99 的随机数;
int var;
while (1) {
cin >> var;
if (var > a) {
cout << " 猜测过大 " << endl;
}
else if (var < a) {
cout << " 猜测过小 " << endl;
}
else {
cout << " 恭喜猜对了 " << endl;
break; // 可以利用 break 关键字退出循环
}
}
system("pause");
}
2.5.2 do while 循环
do {
// 执行一次
}
while (条件);
案例—水仙花数:
水仙花数是指一个 3 位数,它的每个位的数字的 3 次之幂之和等于它本身
例如:1^3 + 5^3 + 3^3 = 153
利用 do while 循环,求出所有 3 位数中的水仙花数。
int main() {
int A = 100;
int a, b, c;
do{
a = A / 100;
b = (A - a * 100) / 10;
c = (A - a * 100 - b * 10);
if (a*a*a + b*b*b + c*c*c == A) {
cout << A << endl;
}
A++;
} while (A < 1000);
system("pause");
}
// 输出 153 370 371 407
注意:C++ 中不能用 ^ 表示幂指数,^ 是位异或的意思。
2.5.3 for 循环
作用:满足循环条件,执行循环语句。
语法:for(起始表达式;条件表达式;末尾循环体){ 循环语句 }
for(int i=0; i<10; i++){
cout << i <<endl;
}
在 C++ 中,for 循环有了一个新用法:for(接受数据的变量名:容器){}
int arr[] = {0,1,2,3,4,5};
for (int n : arr) {
cout << n << " "; // 输出 1 2 3 4 5 6
}
cout << endl;
2.5.4 嵌套循环
int main() {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
cout << "* ";
}
cout << endl;
}
system("pause");
}
2.6 跳转语句
2.6.1 break 语句
作用:用于跳出选择结构或者循环结构。一碰到 break 关键词就 跳出当前循环。
break 使用时机:
- 出现 switch 条件语句中,作用是终止 case 并跳出 switch
- 出现在循环语句中,作用是跳出当前的循环语句
- 出现在嵌套循环中,跳出最近的内层循环语句
2.6.2 continue 语句
作用:在循环语句中,跳出本次循环中余下未执行的语句,继续执行下一次循环。
int main() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 可以筛选条件,执行到此后面的 cout i 就不再执行
}
cout << i << endl;
}
system("pause");
}
// 输出 1 3 5 7 9
2.6.3 goto 语句
作用:可以无条件跳转语句。如果标记名词存在,执行到 goto 语句时,会跳转到标记位置。
语法:goto 标记
int main() {
cout << "1. XXXXX" << endl;
cout << "2. XXXXX" << endl;
goto FLAG;
cout << "3. XXXXX" << endl;
cout << "4. XXXXX" << endl;
FLAG:
cout << "5. XXXXX" << endl;
system("pause");
}
/* 输出
1. XXXXX
2. XXXXX
5. XXXXX
*/
2.7 数组
所谓数组,就是几个集合,里面存放了相同类型的数据类型。
- 数组中每一个数据元素都是相同的数据类型
- 数组是由连续的内存位置组成
2.7.1 创建数组
// 第一种定义方式
int arr[3]; // 声明数组
arr[0] = 1; // 赋值
arr[1] = 2;
arr[2] = 3;
// 第二种定义方式
int arr2[5] = { 1,2,3 };
for (int i = 0; i < 5; i++) {
cout << arr2[i] << endl;
}
/* 输出 1 2 3 0 0 */
// 第三种定义方式
int arr3[] = { 1,2,3,4 };
2.7.2 一维数组
一维数组名称的用途:
获取数组的长度
获取数组在内存中的首地址
int arr[] = { 1,2,3,4 };
cout << sizeof(arr)/sizeof(arr[0]) << endl; // 输出数组长度
cout << arr << endl; // 输出数组的首地址
cout << &arr[0] << endl; // 输出数组第一个元素的地址
& 是取址符
练习案例 - 小猪称体重: 在一个数组中记录了五只小猪的体重,找出并打印最重的小猪的体重。
int main() {
int pigs[] = {250,380,129,566,412};
int max = pigs[0]; // 假设第一个值最大
for (int i = 1; i < (sizeof(pigs) / sizeof(pigs[0])); i++) {
if (pigs[i]>max) {
max = pigs[i]; // 逐一比较,更新最大值
}
}
cout <<" 最大的数值为:" << max << endl;
system("pause");
}
练习案例 - 元素逆置:将一个数组里的元素逆置,并输出逆置后的结果。
int main() {
int a[] = {1,2,3,4,5,6};
int start = 0;
int end = sizeof(a) / sizeof(a[0]) -1;
int temp;
while(start < end){
temp = a[start];
a[start] = a[end];
a[end] = temp;
start++;
end--;
}
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
cout << a[i]<<endl;
}
system("pause");
}
2.7.3 冒泡排序
冒泡排序算法是最常用的算法,对数组内元素进行排序
- 比较相邻元素,如果第一个比第二个大,就交换它们两个
- 对每一对相邻的元素做同样的工作,执行完毕后,找到第一个最大值。
- 重复以上的步骤,每次比较次数减一,直到不需要比较。
int main() {
int a[] = {4,2,8,0,5,7,1,3,9};
int len = sizeof(a) / sizeof(a[0]);
int temp;
// 排序轮数
for (int i = 0; i < len-1; i++) {
// 每轮对比次数
for (int j = 0; j < len-i-1; j++) {
if (a[j] > a[j+1]) {
temp = a[j+1];
a[j+1] = a[j];
a[j] = temp;
}
}
}
for (int i = 0; i < len; i++) {
cout << a[i] << endl;
}
system("pause");
}
2.7.4 二维数组
二维数组定义的四种方式:
数据类型 数组名[行数][列数];
数据类型 数组名[行数][列数] = {{ 数据 1, 数据 2},{ 数据 3, 数据 4}};
数据类型 数组名[行数][列数] = { 数据 1, 数据 2, 数据 3, 数据 4};
数据类型 数组名[][列数] = { 数据 1, 数据 2, 数据 3, 数据 4};
建议采用第二种,更加直观。
int arr[2][3] = { {1,2,3}, {4,5,6} };
cout << sizeof(arr) << endl; // 输出数组所占内存空间大小
cout << sizeof(arr[0]) << endl; // 输出数组第一行所占内存空间大小
int row = sizeof(arr)/sizeof(arr[0]); // 行数
int col = sizeof(arr[0])/sizeof(arr[0][0]); // 列数
cout << arr << endl; // 输出数组的首地址
cout << &arr[0][0] << endl; // 输出数组第一个元素的地址
cout << arr[0]<< endl; // 输出数组第一行的首地址
cout << arr[1]<< endl; // 输出数组第二行的首地址
2.8 函数
2.8.1 函数概述
作用:将一段经常使用的代码封装起来,减少代码重复
函数的定义一般主要由 5 个步骤:
- 返回值类型
- 函数名
- 参数列表
- 函数体语句
- return 表达式
/*
返回值类型 函数名(参数列表){
// 函数体语句;
return 表达式
}
*/
void swap(int a, int b) { // 这里 a,b 均为形参
int temp;
temp = b;
b = a;
a = temp;
}
int main() {
int a = 10;
int b = 20;
swap(a, b); // 这里 a,b 均为实参
cout << a << endl;
cout << b << endl;
return 0; // 可省略
system("pause");
}
// 输出 a,b 为 10,20
如果函数不需要返回值,声明函数的时候可以写void。
当我们在做值传递的时候,函数的形参发生改变,不会影响实参。
2.8.2 函数的声明
由于 C++ 编译器的特点,main 函数一定要位于其调用其他函数的后面。在实际使用过程中,会常将定义的函数在开头做一个声明,从而不用将函数体定义在 main 函数的前面。
int get_max(int a, int b); // 函数的声明,提前告诉编译器有这个函数
int main() {
int a = 10;
int b = 25;
cout << get_max(a, b) << endl;
return 0; // 可省略
system("pause");
}
// 函数的定义
int get_max(int a, int b) {
return a > b ? a : b;
}
函数可以声明写多次,定义只能写一次。
2.8.3 函数的分文件编写
作用:让代码结构更加清晰。
函数分文件编写一般有四个步骤:
- 创建后缀名为.h 的头文件
- 创建后缀名为.cpp 的源文件
- 在头文件中写函数的声明
- 在源文件中写函数的定义,并链接头文件
#include " 头文件名 "
2.8.4 静态变量
静态变量:加关键词 static
的变量。三个特点:
- 作用范围:只本文件中可访问,对其他文件是隐藏的
- 创建与释放:程序开始时分配空间,结束时释放空间,数据存放于内存的全局区中
- 初始化默认为 0,使用时也可对其重新赋值
2.9 指针
指针的作用:可以通过指针间接访问内存。
- 内存编号是从 0 开始记录的,一般用十六进制数字表示
- 可以利用指针变量保存地址
2.9.1 指针变量的定义和使用
指针变量定义的语法:数据类型 * 变量名
int main() {
int a = 10;
int* p; // 定义一个指针 p
p = &a; // 将变量 a 的地址赋值给 p,& 是取址符
int b = *p; // 指针前加 * 代表解引用,找到指针指向内存的数据
return 0; // 可省略
system("pause");
}
指针所占的内存空间:32 位系统—4byte,64 位系统—8byte,无论什么数据类型的指针。
2.9.2 空指针与野指针
空指针:指针变量指向内存中编号为 0 的空间
用途:初始化指针变量
注意:空指针指向的内存是不可以访问的
野指针:指针变量指向非法的内存空间
int* p = NULL; // 初始化指针为空
int* p1 = (int*)0x11001 // 指针指向了无访问权限的地址,野指针
总结:空指针和野指针都不是我们申请的空间,因此不要访问。
2.9.3 const 修饰指针
const 修饰指针有三种情况:
const 修饰指针 — 常量指针:指针的指向可以改,但是指针指向的值不可以改
int a=10; int b=20; const int* p = &a; // 常量指针 *p = 20; // 错误操作,指针指向的值不可更改 p = &b; // 允许操作,指针指向可以改
可以记忆为“常量的指针”,常量即值不可以修改。
const 修饰常量 — 指针常量:指针的指向不可以改,但是指针指向的值可以改
int a=10; int b=20; int* const p = &a; // 指针常量 *p = 20; // 允许操作,指针指向的值可改 p = &b; // 错误操作,指针指向不可以改
可以记忆为“指针是常量”,指针为常量即指针指向不可以改。
const 既修饰指针,又修饰常量:指针的指向不可以改,指针指向的值也不可改
int a=10; int b=20; const int* const p = &a; // *p = 20; // 错误操作,指针指向的值不可改 p = &b; // 错误操作,指针指向不可改
2.9.4 指针和数组
作用:利用指针访问数组中的元素
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr; // 赋值数组首地址给指针 p
cout << *p << endl; // 获取首地址(第一个元素)的值
p++; // 指针右偏移四个字节
cout << *p << endl; // 获取第二个元素的值
system("pause");
}
2.9.5 指针和函数
作用:利用指针作函数参数,可以修改实参的值
void swap(int* p1, int* p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main() {
int a = 1;
int b = 2;
swap(&a, &b); // 地址传递
cout << a << endl; // 输出 a 为 2
cout << b << endl; // 输出 b 为 1
system("pause");
}
地址传递可以改变实参的值。
如果想改变实参的值,就用地址传递;如果不想改变实参,就用值传递。
2.9.6 指针,数组和函数
封装一个函数,利用冒泡排序,实现对整型数组的升序排列。
void bubbleSort(int* arr, int len) {
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (arr[j + 1] < arr[j]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[] = { 4,3,6,9,1,2,10,8,7,5 };
int len = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, len);
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
system("pause");
}
2.10 结构体
2.10.1 基本概念
结构体属于用户自定义的数据类型,允许用户储存不同的数据类型。
语法:struct 结构体名 { 结构体成员列表 };
通过结构体创建变量的方式有三种:
- struct 结构体名,变量名
- struct 结构体名 变量名 = {成员 1 值,成员 2 值…}
- 定义结构体时顺便创建变量
struct student {
string name;
int age;
int scores;
}s3; // 第三种方式:定义结构体时顺便创建变量
int main() {
// 第一种方式
struct student s1; //struct 关键词可以省略
s1.name = "zs";
s1.age = 25;
s1.scores = 80;
// 第二种方式
struct student s2 = {"ls", 27, 90};
s3.name = "we";
s3.age = 20;
s3.scores = 52;
cout << s1.name << endl; // 通过. 点来访问成员
cout << s2.name << endl;
cout << s3.name << endl;
system("pause");
return 0;
}
2.10.2 结构体数组
作用:将自定义的结构体放入到数组中方便维护
语法:struct 结构体名 数组名[] = {{},{},{}}
struct student {
string name;
int age;
int scores;
};
int main() {
struct student s1[] = {
{"zs", 19, 68},
{"li", 25, 70},
{"dw", 26, 85},
};
system("pause");
return 0;
}
2.10.3 结构体指针
作用:通过指针访问结构体中的成员
- 利用操作符
->
可以通过结构体指针访问结构体属性。
struct student {
string name;
int age;
int scores;
};
int main() {
struct student s1 = { "zs", 19, 68 };
student * p = &s1; // 创建结构体指针并赋值
cout << p->name << endl; // 访问属性
system("pause");
return 0;
}
2.10.4 结构体嵌套
在结构体中可以定义另一个结构体作为成员,用来解决实际问题。
struct student {
string name;
int age;
int scores;
};
struct teacher{
string name;
int age;
string course;
struct student stu; // 嵌套一个 student 结构体
};
int main() {
teacher t1;
t1.name = "wteacher";
t1.age = 45;
t1.course = "mathematics";
t1.stu.name = "zs";
t1.stu.age = 25;
t1.stu.scores = 80;
system("pause");
return 0;
}
2.10.5 结构体做函数参数
作用:将结构体作为参数向函数中传递
传递方式有两种:
- 值传递
- 地址传递
struct student {
string name;
int age;
int scores;
};
// 值传递
void printstudent1(struct student s) {
s.age = 80;
cout << s.name <<" ";
cout << s.age << " ";
cout << s.scores <<endl;
};
// 地址传递
void printstudent2(struct student* s) {
s->age = 100;
cout << s->name << " ";
cout << s->age << " ";
cout << s->scores << endl;
};
int main() {
student s1 = { "zs", 19, 68 };
printstudent1(s1); // 输出 "zs", 80, 68
cout << s1.name << " ";
cout << s1.age << " ";
cout << s1.scores << endl; // 输出 "zs", 19, 68 不改变原来的值
printstudent2(&s1); // 输出 "zs", 100, 68
cout << s1.name << " ";
cout << s1.age << " ";
cout << s1.scores << endl; // 输出 "zs", 100, 68 改变原来的值
system("pause");
return 0;
}
2.10.6 结构体中 const 使用场景
在实际使用场景中,若使用值传递,相当于又要拷贝一份数据给函数,会显著增加内存资源损耗。使用地址传递就不会存在这个问题,一个指针只占四个字节,会极大的节省空间,但是地址传递在新的函数中处理数据时,会改变原来数据,这时候就可以使用 const 修饰,避免数据篡改。
void printstudent1(const struct student* s) { // 传地址时加 const 限制
s->age = 100; // 此修改行为不允许
cout << s->name << " ";
cout << s->age << " ";
cout << s->scores << endl;
};
int main() {
student s1 = { "zs", 19, 68 };
printstudent1(&s1);
system("pause");
return 0;
}
2.10.6 案例
案例描述:设计一个英雄的结构体,包括成员姓名,性别;创建结构体数组,数组中存放 5 名英雄。
通过冒泡排序算法,将数组中的英雄按照年龄进行升序排列,最终打印排序后的结果。
struct Hero {
string name;
int age;
string sex;
};
void bubblesort(struct Hero sanguo[],int len) {
for (int i = 0; i < len-1; i++) {
for (int j = 0; j < len-1-i; j++) {
if (sanguo[j].age > sanguo[j + 1].age) {
struct Hero temp = sanguo[j];
sanguo[j] = sanguo[j + 1];
sanguo[j + 1] = temp;
}
}
}
}
int main() {
Hero sanguo[] = {
{" 刘备 ",23," 男 "},
{" 关羽 ",22," 男 "},
{" 张飞 ",20," 男 "},
{" 赵云 ",21," 男 "},
{" 貂蝉 ",19," 女 "},
};
int len = sizeof(sanguo) / sizeof(sanguo[0]);
bubblesort(sanguo, len);
for (int i = 0; i < len - 1; i++) {
cout << sanguo[i].name << " ";
cout << sanguo[i].age << " ";
cout << sanguo[i].sex << endl;
}
system("pause");
return 0;
}
- 结构体变量作函数参数时,函数内的操作不会改变结构体的值,结构体的各成员作为实参传递给了函数的形参,实际操作的是形参,不会影响实参;
- 结构体数组作为函数参数时,实际上是将结构体数组的第一个数组成员的地址传递给了形参,用对应的指针或者直接用结构体数组的名称作为实参效果是一样的,操作都直接对结构体数组进行,可以改变其值。
2.11 枚举类型
枚举类型 (enumeration) 是 C++ 中的一种派生 数据类型,它是由用户定义的若干枚举常量的集合。
2.11.1 定义枚举类型
enum 数据类型名 { 枚举常量表 };
# 举例
enum Week {Mon, Tus, Wed, Thus, Fri, Sat, Sun}
语句将创建一个名为 Week 的数据类型—枚举类型(与整型,浮点型等类似)。
枚举常量表——由枚举常量构成,以标识符形式表示的整型量,而不能是整型、字符型等文字常量。
枚举常量代表该枚举类型的变量可能取的值,默认情况下,编译系统为每个枚举常量指定一个整数值,从 0 开始,依次加 1;也可自行指定。
若自行指定,而指定值之后的枚举常量按依次加 1 的原则取值。 各枚举常量的值可以重复,枚举标识符不能重复。
enum letter_set {'a','d','F','s','T'}; // 非法
enum year_set{2000,2001,2002,2003,2004,2005}; // 非法
enum fruit_set {apple, orange, banana=1, peach, grape} // 合法
# apple=1, orange=2, banana=1, peach=2, grape=3
2.11.2 定义枚举变量
定义枚举数据类型后,可以接着使用枚举类型指定枚举变量。
// 定义枚举类型
enum Week {Mon, Tus, Wed, Thus, Fri, Sat, Sun};
// 定义枚举变量 w1 和 w2, 并赋值 w2
Week w1, w2 = Tus;
也可以:类型与变量同时定义(甚至类型名可省)
enum {Mon, Tus, Wed, Thus, Fri, Sat, Sun} w1, w2;
枚举变量的值只能取:枚举常量表中所列的标识符。虽然枚举常量表中的标识符中的背后代表的是枚举常量,但是枚举变量的值不能取整型常量值,如 1,2 等。
枚举变量占用内存的大小与整型数相同。不管枚举类型有多少枚举量,枚举数都占 4 bytes.
枚举变量只能参与 赋值 和关系运算 以及 输出操作,其中参与运算时用其本身的整数值。
允许的赋值操作如下:
enum {Mon, Tus, Wed, Thus, Fri, Sat, Sun} w1, w2;
w1 = Fri;
w2 = w1;
int i = w2;
int j = Sun;
非法操作:
enum {Mon, Tus, Wed, Thus, Fri, Sat, Sun} w1, w2;
w1 = 1; // 非法
w2 = FFF; // 非法
2.11.3 关系运算
可以使用整数值而不是符号名称来测试枚举变量。还可以使用关系运算符来比较两个枚举变量
enum { Mon, Tus, Wed, Thus, Fri, Sat, Sun } w1 = Mon, w2 = Fri;
if (w1 == Mon) {
cout << "w1 的枚举变量名是 Mon" << endl;
}
if (w1 == 0) {
cout << "w1 的枚举常量是 0" << endl;
}
if (w2 > w1) {
cout << "w2 的枚举常量大于 w2" << endl;
}
2.11.4 枚举类
C++11 中新增了枚举类,也称作【限定作用域的枚举类】。关键字为:enum class
enum 现在被称为【不限范围】的枚举型别。
enum class 是【限定作用域】枚举型别,他们仅在枚举型别内可见,且只能通过强制转换转换为其他型别。
两种枚举都支持底层型别指定,enum class 默认是 int,enum 没有默认底层型别。enum 可以前置声明,但仅在指定默认底层型别的情况下才能前置声明。
枚举类的基本用法和枚举数一致。
枚举类优势:
降低命名空间污染
// 枚举数 enum Week{ Mon, Tus, Wed, Thus, Fri, Sat, Sun } w1 = Mon, w2 = Fri; int Mon = 100; // 错误 // 枚举类 enum class Month { Jan, Feb, Mar, Apr, May } m1 = Month::Jan, m2; m2 = Month::May; int Jan = 100; // 允许
避免发生隐式转换
// 枚举数 enum Week{ Mon, Tus, Wed, Thus, Fri, Sat, Sun } w1 = Mon; if (w1 < 7) {} // 枚举类 enum class Month { Jan, Feb, Mar, Apr, May } m1 = Month::Jan; if (m1 < 7) {} // 不允许
限定作用域的枚举型别不允许发生任何隐式转换。如果非要转换,按就只能使用 static_cast 进行强制转换。
可以前置声明
enum Color; // 非法 enum class Color; // 合法
三、实战 1- 通讯录管理系统
通讯录是一个可以记录亲人,好友信息的工具。系统中需要实现的功能如下:
- 添加联系人:向通讯录中添加新人,信息包括(姓名,性别,年龄,联系电话,家庭住址),最多记录 100 人
- 显示联系人:显示通讯录总所有联系人的信息
- 删除联系人:按照姓名进行删除指定联系人
- 查找联系人:按照姓名进行查找指定联系人
- 修改联系人:按照姓名重新修改指定联系人
- 清空联系人:清空通讯录中所有信息
- 退出通讯录:退出当前使用的通讯录
#include<iostream>
#include<string>
using namespace std;
#define MAX 1000
void showMenu() {
cout << "**************************" << endl;
cout << "***** 1. 添加联系人 *****" << endl;
cout << "***** 2. 显示联系人 ***** " << endl;
cout << "***** 3. 删除联系人 *****" << endl;
cout << "***** 4. 查找联系人 *****" << endl;
cout << "***** 5. 修改联系人 *****" << endl;
cout << "***** 6. 清空联系人 *****" << endl;
cout << "***** 0. 退出通讯录 *****" << endl;
cout << "**************************" << endl;
}
struct member {
string name;
int sex; //1 为男 2 为女
int age;
string tel;
string addr;
};
struct addressbooks {
struct member memberarr[MAX]; // 通讯录名单
int size; // 人数
};
void addperson(addressbooks * abs) {
if (abs->size == MAX) {
cout << " 联系人已满,请删除不必要的人员再添加 " << endl;
return;
}
else {
string name;
int sex=0; //1 为男 2 为女
int age=0;
string tel;
string addr;
cout << " 请输入姓名:" << endl;
cin >> name;
abs->memberarr[abs->size].name = name;
cout << " 请输入性别:" << endl;
cout << "1 --- 男 " << endl;
cout << "2 --- 女 " << endl;
while (true) {
cin >> sex;
if (sex == 1 || sex == 2) {
abs->memberarr[abs->size].sex = sex;
break;
}
cout << " 输入有误,请重新输入!" << endl;
}
cout << " 请输入年龄:" << endl;
cin >> age;
abs->memberarr[abs->size].age = age;
cout << " 请输入电话:" << endl;
cin >> tel;
abs->memberarr[abs->size].tel = tel;
cout << " 请输入地址:" << endl;
cin >> addr;
abs->memberarr[abs->size].addr = addr;
abs->size++;
cout << " 添加成功 " << endl;
system("pause");
system("cls");
}
}
void showperson(addressbooks * abs) {
if (abs->size == 0) {
cout << " 通讯录为空 " << endl;
}
else {
for (int i = 0; i < abs->size; i++) {
cout << " 姓名: " << abs->memberarr[i].name;
cout << "\t 性别: " << (abs->memberarr[i].sex==1?" 男 ":" 女 ");
cout << "\t 年龄: " << abs->memberarr[i].age;
cout << "\t 电话: " << abs->memberarr[i].tel;
cout << "\t 地址: " << abs->memberarr[i].addr << endl;
}
}
system("pause");
system("cls");
}
int isExist(addressbooks* abs, string name) {
for (int i = 0; i < abs->size; i++) {
if (abs->memberarr[i].name == name) {
return i;
}
}
return -1;
}
void delperson(addressbooks* abs) {
cout << " 请输入删除的联系人:" << endl;
string name;
cin >> name;
int ret = isExist(abs,name);
if (ret != -1) {
for (int i = ret; i < abs->size; i++) {
abs->memberarr[i] = abs->memberarr[i + 1];
}
abs->size--;
cout << " 删除成功 " << endl;
}
else {
cout << " 查无此人 " << endl;
}
system("pause");
system("cls");
}
void findperson(addressbooks* abs) {
cout << " 请输入查找的联系人:" << endl;
string name;
cin >> name;
int ret= isExist(abs, name);
if (ret != -1) {
cout << " 姓名: " << abs->memberarr[ret].name << "\t";
cout << " 性别: " << (abs->memberarr[ret].sex == 1 ? " 男 " : " 女 ") << "\t";
cout << " 年龄: " << abs->memberarr[ret].age << "\t";
cout << " 电话: " << abs->memberarr[ret].tel << "\t";
cout << " 地址: " << abs->memberarr[ret].addr << endl;
}
else {
cout << " 查无此人 " << endl;
}
system("pause");
system("cls");
}
void modifyperson(addressbooks* abs) {
cout << " 请输入修改的联系人:" << endl;
string name;
cin >> name;
int ret = isExist(abs, name);
if (ret != -1) {
cout << " 请输入姓名:" << endl;
cin >> name;
abs->memberarr[ret].name = name;
cout << " 请输入性别:" << endl;
cout << "1 --- 男 " << endl;
cout << "2 --- 女 " << endl;
while (true) {
int sex = 0;
cin >> sex;
if (sex == 1 || sex == 2) {
abs->memberarr[abs->size].sex = sex;
break;
}
cout << " 输入有误,请重新输入!" << endl;
}
int age = 0;
cout << " 请输入年龄:" << endl;
cin >> age;
abs->memberarr[ret].age = age;
cout << " 请输入电话:" << endl;
string tel;
cin >> tel;
abs->memberarr[ret].tel = tel;
cout << " 请输入地址:" << endl;
string addr;
cin >> addr;
abs->memberarr[ret].addr = addr;
cout << " 修改成功 " << endl;
}
else {
cout << " 查无此人 " << endl;
}
system("pause");
system("cls");
}
void cleanperson(addressbooks* abs) {
abs->size = 0;
cout<<" 联系人已全部清空 " << endl;
system("pause");
system("cls");
}
int main() {
int select = 0;
addressbooks abs;
abs.size = 0;
while (true) {
showMenu();
cin >> select;
switch (select)
{
case 1:
addperson(&abs);
break;
case 2:
showperson(&abs);
break;
case 3:
delperson(&abs);
break;
case 4:
findperson(&abs);
break;
case 5:
modifyperson(&abs);
break;
case 6:
cleanperson(&abs);
break;
case 0:
cout << " 欢迎下次使用 " << endl;
system("pause");
return 0 ;
break;
default:
break;
}
}
}
四、C++ 核心编程
本阶段主要针对 C++面向对象编程 技术做详细讲解,探讨 C++ 中的核心和精髓。
4.1 内存分区模型
C++ 程序在执行时,将内存大方向划分为 4 个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时,由操作系统回收。
内存四区的意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
—————–程序运行前————–
在程序编译后,生成 exe 可执行程序,未执行该程序前分为两个区域:
- 代码区:存放 CPU 执行的机器指令;代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份即可;代码区是只读的,防止程序意外的修改了它的指令。
- 全局区:全局变量 和静态变量 存放于此;全局区还包含了 常量 区,字符串常量和其他常量也存放于此;该区域的数据在程序结束后由操作系统释放。
int g_a = 10; // 全局变量
const int c_g_a = 10; //const 修饰的全局变量
int main(){
int a = 10; // 局部变量
static int s_a = 10; // 静态变量,在普通变量前加 static,属于静态变量
string str_a = "hello word"; // 字符串常量
const int c_a = 10; //const 修饰的局部变量
system("pause");
}
—————–程序运行中————–
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。注意:不要返回局部变量的地址。
堆区:由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。在 C++ 中,主要利用 new 关键字在堆区开辟内存。
int* func() {
int* p = new int(10); //new 创建的数据,返回指针
return p;
}
int main(){
int* p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
}
4.2 new 操作符
C++ 中利用 new 操作符在堆区开辟数据.
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
语法:new 数据类型
利用 new 创建的数据,会返回该数据对应的类型的指针。
int* func() {
int* arr = new int[10]; // 创建一个数组,返回首地址
arr[0] = 0;
return arr;
}
int main(){
int* p = func();
cout << *p << endl;
cout << *p << endl;
delete[] p; // 释放数组时,要加[]
system("pause");
}
4.3 引用
作用:给变量起别名
语法:数据类型 & 别名 = 原名
int main(){
int a = 10;
int& b = a;
cout << a << endl; // 输出 10
cout << b << endl; // 输出 10
b = 100;
cout << a << endl; // 输出 100
cout << b << endl; // 输出 100
system("pause");
}
注意事项:引用必须初始化,且在初始化后不可更改。
4.3.1 引用作函数实参
作用:函数传参时,可以利用引用让形参修饰实参
优点:可以简化指针修改实参
void swap(int &a,int &b){
int temp = a;
a = b;
b = temp;
}
int main(){
int a = 10;
int b = 20;
swap(a, b); // 引用传递,形参也会修饰实参的
cout << a << endl; // 输出 20
cout << b << endl; // 输出 10
system("pause");
}
总结:通过引用参数产生的效果同按照地址是一样的,引用的语法更加清楚简单。
4.3.2 引用作函数的返回值
作用:引用是可以作为函数的返回值存在的。注意:不要返回局部变量引用
用法:函数调用作为左值
int& test() {
static int a = 10;
return a; // 返回静态变量的引用
}
int main(){
int& ref = test();
cout << ref << endl;
cout << ref << endl;
// 如果函数作左值,必须返回函数的引用
test() = 1000;
cout << ref << endl;
cout << ref << endl;
system("pause");
}
4.3.3 引用的本质
本质:引用的本质在 C++ 内部实现的一个指针常量
void func(int& ref) {
ref = 50; //ref 是引用,转换为 *ref = 50
}
int main() {
int a = 10;
int& ref = a; // 自动转换为 int* const ref=&a; 指针常量时指针指向不可改,也说明为什么引用不可更改
ref = 100; // 内部发现 ref 是引用,自动帮我们转换为 *ref=100;
func(a);
}
C++ 推荐使用引用技术,因为语法方便,引用本质是指针常量,但所有的指针操作编译器都帮我们做了。
4.3.4 常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加 const 修饰形参,防止形参改变实参
void func(const int& v) {
v = 50; // 常量引用不可赋值,报错
cout << v << endl;
}
int main() {
//int& ref = 10; // 报错。常量引用,引用必须引一块合法的内存空间
const int& ref = 10;
// int temp=10; const int& ref = temp; // 实际是这样的
//ref = 20; 加入 const 之后变为只读,不可以修改
// 函数中利用常量引用防止误操作修改实参
int a = 20;
func(a);
system("pause");
}
4.4 函数的提高
4.4.1 函数默认值
在 C++ 中,函数的形参列表中的形参是可以有默认值的
语法:返回值类型 函数名 (参数 = 默认值){}
// 定义函数时,可以设置初始默认值
int func(int a, int b = 20,int c = 30) {
return (a + b + c);
}
int main() {
cout << func(10) << endl;
cout << func(10, 40) << endl; // 如果有传参,就用传的参数,若无,就用默认的值
cout << func(10, 40, 50)<< endl;
//cout << func(, , 50)<< endl; // 非法调用,不能只默认前两个,改第三个
system("pause");
}
定义函数时,如果某个位置已经有了默认值,那么从这个位置往后,从左到右都必须有默认值。
函数声明和函数定义,二者只能有一种默认参数,不能函数声明和函数定义都写默认参数。
int func(int a, int b = 30,int c = 0) // 函数声明
int func(int a, int b = 30,int c = 0) { // 函数定义
return (a + b + c);
}
// 以上的默认值写法是错的,不能同时存在
4.4.2 函数占位参数
C++ 中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法:返回值类型 函数名 (数据类型){}
void func(int a, int,int) {
cout << "this is a test" << endl;
}
int main() {
func(10, 10, 10);
system("pause");
}
4.4.3 函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名相同
- 函数参数 类型不同 或者 个数不同 或者 顺序不同
void func() {
cout << "func()的调用 " << endl;
}
void func(int a) {
cout << "func(int a)的调用 " << endl;
}
int main() {
func(); // 输出:func()的调用
func(10); // 输出:func(int a)的调用
system("pause");
}
函数的返回值不可以作为函数重载的条件
函数参数引用可以作为函数重载的条件
使用函数重载时,尽量不要写默认参数
void func(int& a) {
cout << "func1 的调用 " << endl;
}
void func(const int &a) {
cout << "func2 的调用 " << endl;
}
int main() {
int a = 10;
func(a); // 输出:func1 的调用
const int b = 10;
func(b); // 输出:func2 的调用
system("pause");
}
4.5 类和对象
C++ 面向对象的三大特性:封装,继承,多态。
C++ 认为万事万物都皆为对象,对象上有其属性和行为
4.5.1 封装
封装是 C++ 面向对象三大特性之一
封装的意义一:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法:class 类名 { 访问权限:属性 / 行为 };
一个圆类:
const double PI = 3.14;
class Circle {
public: // 访问权限:公共权限
int m_r;
double calcul_perimeter() {
return 2 * PI * m_r;
}
};
int main() {
Circle C1; // 通过圆类,创建具体的圆(对象),实例化
C1.m_r = 10; // 给属性赋值
cout << C1.calcul_perimeter() << endl; // 输出:62.8
system("pause");
}
类中的属性和行为,我们统一称为成员。
属性:成员属性、成员变量
行为:成员函数、成员方法
封装的意义二:
- 类在设计时,可以把属性和行为放在不同的权限下,加以控制
- 访问权限有三种
- public 公共权限:类内可以访问,类外可以访问
- protected 保护权限:类内可以访问,类外不可以访问
- private 私有权限:类内可以访问,类外不可以访问
class Person {
public:
string m_name;
protected:
string m_car;
private:
int m_password = 1001;
public:
void get_info() {
//m_name = "liu wen";
m_car = "BWM";
m_password = 123456;
cout << m_name << endl;
cout << m_car << endl;
cout << m_password << endl;
}
};
int main() {
Person p1;
p1.m_name = "li qiang";
p1.get_info(); // 输出:li qiang BWM 123456
system("pause");
}
struct 和 class 区别:
在 C++ 中,struct 和 class 唯一的区别就在于默认的访问权限不同。
区别:
- struct 默认权限为公共 public
- class 默认权限为私有 private
成员属性设为私有
- 将所有成员属性设为私有,可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
举一个例子:
class Person {
public:
void setname(string name) {
m_name = name;
}
string getname() {
return m_name;
}
string getcar() {
string m_car = "BWM"; // 初始化
return m_car;
}
void setpassword(int password) {
m_password = password;
}
private:
string m_name;
string m_car;
int m_password;
};
int main() {
Person p1;
p1.setname("li qiang"); // 可写
cout << p1.getname() << endl; // 可读
cout << p1.getcar() << endl; // 只读
p1.setpassword(456789); // 只写
system("pause");
}
一个正方体的例子:
class Cube {
public:
void setL(int L) {
m_L = L;
}
double getL() {
return m_L;
}
void setW(int W) {
m_W = W;
}
double getW() {
return m_W;
}
void setH(int H) {
m_H = H;
}
double getH() {
return m_H;
}
bool isSame(Cube& c) {
if (m_L == c.getL() && m_W == c.getW() && m_H == c.getH()) {
return true;
}
return false;
}
private:
double m_L;
double m_W;
double m_H;
};
bool isSame(Cube& c1, Cube& c2) {
if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()) {
return true;
}
return false;
}
int main() {
Cube C1;
C1.setL(100);
C1.setH(100);
C1.setW(100);
Cube C2;
C2.setL(100);
C2.setH(100);
C2.setW(100);
// 成员函数判断
bool tag = C1.isSame(C2);
if (tag) {
cout << "C1 和 C2 是相同的 " << endl;
}
else {
cout << "C1 和 C2 是不同的 " << endl;
}
// 全局函数判断
isSame(C1, C2);
system("pause");
}
4.5.2 对象的初始化和清理
4.5.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知的
同样使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题
C++ 利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象成员属性赋值,构造函数由编译器自动调用,无须手动调用。在创建对象时调用。
- 析构函数:主要作用在于对象销毁系统自动调用,执行一些清理工作。在销毁对象前调用。
构造函数语法:类名 (){}
- 构造函数,没有返回值也不写 void
- 构造函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用构造,无须手动调用而且只会调用一次
析构函数语法:~ 类名(){}
- 析构函数,没有返回值也不写 void
- 析构函数名称与类名相同,在名称前加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在调用对象时会自动调用析构,无须手动调用而且只会调用一次
class Person {
public:
//Person 类的构造函数
Person() {
cout << "Person 构造函数的调用 " << endl;
}
//Person 类的析构函数
~Person() {
cout << "Person 析构函数的调用 " << endl;
}
};
void test() {
Person p; // 在栈上的数据,函数执行完会被自动释放
}
int main() {
test(); // 构造,析构函数均会被调用
Person p1; // 这里不会析构,p1 执行完没被释放,按 enter 后才被释放,调用析构
system("pause");
}
4.5.2.2 构造函数的分类及调用
两种分类方式:
- 按参数分:有参构造和无参构造
- 按类型分:普通构造和拷贝构造
三种调用方式:括号法 显示法 隐式转换法
class Person {
public:
//Person 类的构造函数
Person() {
cout << " 默认构造函数的调用 " << endl;
}
//Person 类的构造函数的重载 --- 有参构造
Person(int a) {
age = a;
cout << " 有参构造函数的调用 " << endl;
}
//Person 类的构造函数的重载 --- 拷贝构造
Person(const Person &p) {
age = p.age;
cout << " 拷贝构造函数的调用 " << endl;
}
//Person 类的析构函数
~Person() {
cout << "Person 析构函数的调用 " << endl;
}
int age;
int name;
};
int main() {
// 括号调用法
Person p1; // 无参构造调用,注意:无参构造调用不能写括号 p1()
//Person p4(); 这种写法会让编译器认为这是函数声明
Person p2(10); // 有参构造调用
Person p3(p2); // 拷贝参构造调用
// 显示法
Person p1;
Person p2 = Person(10); //Person(10); 匿名对象,执行完系统会立即回收
Person p3 = Person(p2);
// 隐式转换法
Person p2 = 10; // 相当于 Person p2(10)
Person p3 = p2; // 拷贝构造
system("pause");
}
4.5.2.3 拷贝构造函数调用时机
C++ 中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回给局部对象
class Person {
public:
Person() {
cout << " 默认构造函数的调用 " << endl;
}
//Person 类的构造函数的重载 --- 拷贝构造
Person(int a) {
age = a;
cout<< " 有参构造函数的调用 " << endl;
}
Person(const Person &p) {
age = p.age;
cout << " 拷贝构造函数的调用 " << endl;
}
//Person 类的析构函数
~Person() {
cout << "Person 析构函数的调用 " << endl;
}
int age;
int name;
};
//1、使用一个已经创建完毕的对象来初始化一个新对象
void test1() {
Person p1(20);
Person p2(p1);
cout << p2.age << endl;
}
//2、值传递的方式给函数参数传值
void doWork2(Person p) {
}
void test2() {
Person p1;
doWork2(p1);
}
//3、以值方式返回给局部对象
Person doWork3() {
Person p1;
cout << (int*)&p1 << endl;
return p1;
}
void test3() {
Person p = doWork3();
cout << (int*)&p << endl;
}
int main() {
test1();
cout << "----------" << endl;
test2();
cout << "----------" << endl;
test3();
cout << "----------" << endl;
system("pause");
}
4.5.2.4 构造函数调用规则
默认情况下,C++ 编译器至少给一个类添加 3 个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数的调用规则如下:
- 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数。
- 总的来说,必须写拷贝构造函数,有它就行。
4.5.2.5 深浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
默认采用类的拷贝构造函数—浅拷贝:
class Person {
public:
Person() {
cout << " 默认构造函数的调用 " << endl;
}
Person(int a,int b) {
age = a;
height = new int(b);
cout<< " 有参构造函数的调用 " << endl;
}
// 析构函数,将堆区开辟的数据做释放操作
~Person() {
if (height != NULL) {
delete height;
height = NULL;
}
cout << "Person 析构函数的调用 " << endl;
}
int age;
int *height;
};
void test1() {
Person p1(18,160);
cout << p1.age << endl;
cout << *p1.height << endl;
Person p2(p1); // 默认浅拷贝
cout << p2.age << endl;
cout << *p2.height << endl;
}
int main() {
test1(); // 浅拷贝带来的问题是内存的重复释放,造成程序异常终止
system("pause");
}
自己写类的拷贝构造函数—深拷贝:
class Person {
public:
Person() {
cout << " 默认构造函数的调用 " << endl;
}
Person(int a,int b) {
age = a;
height = new int(b);
cout<< " 有参构造函数的调用 " << endl;
}
// 自己写拷贝构造函数
Person(const Person& p) {
age = p.age;
//height = p.height; 编译器默认写法
height = new int(*p.height); // 深拷贝
}
// 析构函数,将堆区开辟的数据做释放操作
~Person() {
if (height != NULL) {
delete height;
height = NULL;
}
cout << "Person 析构函数的调用 " << endl;
}
int age;
int *height;
};
void test1() {...}
int main() {...}
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
4.5.2.6 初始化列表
作用:c++ 提供了初始化列表语法,用来初始化属性
语法:构造函数():属性 1(值 1),属性 2(值 2){}
class Person {
public:
Person() : A(0), B(0), C(1){ // 初始化默认属性
cout << " 构造函数的调用 " << endl;
cout << A << endl;
cout << B << endl;
cout << C << endl;
}
Person(int a, int b, int c) : A(a), B(b), C(c){ // 通过传参初始化属性
cout << " 构造函数的调用 " << endl;
cout << A << endl;
cout << B << endl;
cout << C << endl;
}
~Person() {
cout << " 析构函数的调用 " << endl;
}
private:
int A;
int B;
int C;
};
int main() {
Person p(10,20,30);
system("pause");
}
4.5.2.7 类对象作为类成员
C++ 类中的成员可以是另一个类的对象,我们称为该成员为 对象成员
class Phone {
public:
string phone_name;
Phone(string pname) {
phone_name = pname;
}
~Phone() {
cout << "Phone 析构函数的调用 " << endl;
}
};
class Person {
public:
Person(int a,string b) : m_id(a), m_phone(b)
{
cout << m_id << endl;
cout << m_phone.phone_name << endl;
}
~Person() {
cout << "Person 析构函数的调用 " << endl;
}
private:
int m_id;
Phone m_phone; // 对象成员
};
int main() {
Person p(111, "iphone13");
system("pause");
}
4.5.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字 static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 只能访问静态成员变量
class Person {
public:
static void func(){
cout << " 静态成员函数的调用 " << endl;
cout << m_A << endl;
//cout << m_B << endl; 不能访问 m_B, 静态成员函数只能访问静态成员变量
}
static int m_A; // 类内声明
int m_B;
};
int Person::m_A = 0; // 类外初始化
int main() {
// 通过对象访问
Person p;
p.func();
// 通过类名访问
Person::func();
system("pause");
}
static 在 C++ 中的作用:
- 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
- static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
- static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
- 不想被释放的时候,可以使用 static 修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
- 考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。
引用:C/C++ 中 static 的用法全局变量与局部变量 | 菜鸟教程 (runoob.com)
4.5.2.9 const 成员
const 对象只能调用 const 成员函数;非 const 对象是可以调用 const 成员函数的。
4.5.3 C++ 对象模型和 this 指针
4.5.3.1 成员变量和成员函数分开储存
在 C++ 中,类内的成员变量和成员函数分开储存
只有非静态成员变量才属于类的对象
首先举一个简单例子:一个空类占的内存空间
class Person {
};
void test1() {
Person p1;
cout << sizeof(p1) << endl; // 输出结果 1,C++ 编译器会给每个空对象分配 1 字节空间,是为了区分空对象占内存的位置
}
int main() {
test1();
system("pause");
}
非空类占的内存空间:非静态成员变量才属于类的对象,按 成员变量计算内存空间;静态变量不属于,不计算内存。
class Person {
public:
void func() { // 成员函数分开储存
cout << m_A << endl;
}
int m_A; // 计算成员变量 内存空间 4
static int m_B; // 不计算
};
void test1() {
Person p1;
cout << sizeof(p1) << endl; // 输出结果 4
}
int main() {
test1();
system("pause");
}
4.5.3.2 this 指针
每一个非静态成员函数只会诞生一份函数实例,也就是多个同类的对象会共用一块代码,那么这一块代码是如何区分每个对象调用自己的呢?
C++ 通过提供特殊的对象指针,this 指针,解决上述问题,this 指针指向被调用的成员函数所属的对象
this 指针是隐含每一个非静态成员函数内的一种指针
this 指针不需要定义,直接使用即可
this 指针的用途:
- 当形参和成员变量同名时,可以用 this 指针来区分
- 在类的非静态成员函数中返回对象本身,可使用 return *this
class Person {
public:
Person(int age) {
this->age = age; // 形参 age 和成员变量 age 同名
}
Person& addAge(Person &p) {
this->age += p.age;
return *this; // 可以链式调用
}
int age;
};
void test1() {
Person p1(18);
cout << p1.age << endl;
}
void test2() {
Person p1(10);
Person p2(10);
p2.addAge(p1).addAge(p1).addAge(p1);
cout << p2.age << endl;
}
int main() {
test1();
test2();
system("pause");
}
4.5.3.3 空指针访问成员函数
C++ 中空指针也是可以调用成员函数的,但是也要注意有没有用到 this 指针
如果用到 this 指针,需要加以判断保证代码的健壮性
class Person {
public:
void func() {
cout << " 调用成员函数 " << endl;
}
void getAge() {
cout << age << endl; // 等同于 this->age,传入的指针为 NULL
}
void getName() {
if (this == NULL) { // 加个判断,避免传入空指针
return;
}
cout << name << endl;
}
int age;
string name;
};
void test1() {
Person* p = NULL;
p->func();
//p->getAge(); 报错
p->getName();
}
int main() {
test1();
system("pause");
}
4.5.3.4 const 修饰成员函数
常函数:
- 成员函数后加 const 后我们称之为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字 mutable 后,在常函数中依然可以修改
常对象:
- 声明对象前加 const 称该对象为常对象
- 常对象只能调用常函数
class Person {
public:
// 在成员函数后面加 const,称为常函数,修饰的是 this 指向,让指针指向的值不可以修改
void getAge() const{
//this->age = 100; // 报错
this->name = "ssss"; //name 是 mutable 属性,可以修改
}
void func1() {
this = NULL; // 报错,this 本质是指针常量,不可以修改指针的指向
}
void func2() {
age = 100;
}
int age;
mutable string name; // 特殊变量,即使在常函数中,也可以修改这个值
};
void test1() {
const Person p; // 常对象
p.age = 100; // 报错,也不可修改
p.name = "sd"; // 可以修改
// 常对象只能调用常函数
p.getAge();
p.func2(); // 报错
}
int main() {
test1();
system("pause");
}
4.5.4 友元
在程序里,有的私有属性也想让一些类外特殊的一些函数或者类进行访问,就需要用到 友元 的技术
友元的目的就是让一个函数或者类,访问另一个类中私有属性。一个类中可以有多个友元。
友元的关键字为friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元:
class Building {
friend void Goodgay(Building& b); // 声明该函数为 Building 类的友元, 可以访问 Building 的私有属性
public:
Building() {
m_sittingroom = " 客厅 ";
m_bedgroom = " 卧室 ";
}
string m_sittingroom;
private:
string m_bedgroom;
};
void Goodgay(Building &b) {
cout << b.m_sittingroom << endl;
cout << b.m_bedgroom << endl; // 访问 Building 类的私有属性
}
void test1() {
Building b1;
Goodgay(b1);
}
int main() {
test1();
system("pause");
}
类做友元:
class Building {
friend class Goodgay; // 友元可以访问 Building 的私有成员
public:
Building();
string m_sittingroom;
private:
string m_bedroom;
};
class Goodgay {
public:
Goodgay();
void visit();
Building *b;
};
// 类外写成员函数
Building::Building() {
m_sittingroom = " 客厅 ";
m_bedroom = " 卧室 ";
}
Goodgay::Goodgay(){
b = new Building;
}
void Goodgay::visit() {
cout << b->m_sittingroom << endl;
cout << b->m_bedroom << endl;
}
void test1() {
Goodgay gg;
gg.visit();
}
int main() {
test1();
system("pause");
}
成员函数做友元:
class Building;
class Goodgay {
public:
Goodgay();
void visit();
Building *b;
};
class Building {
friend void Goodgay::visit(); // 成员函数做友元
public:
Building();
string m_sittingroom;
private:
string m_bedroom;
};
// 类外写成员函数
Building::Building() {
m_sittingroom = " 客厅 ";
m_bedroom = " 卧室 ";
}
Goodgay::Goodgay(){
b = new Building;
}
void Goodgay::visit() {
cout << b->m_sittingroom << endl;
cout << b->m_bedroom << endl; // 可以访问私有属性
}
void test1() {
Goodgay gg;
gg.visit();
}
int main() {
test1();
system("pause");
}
注意 Building 类必须写在 Goodgay 类后面
关于友元,有两点需要说明:
- 友元的关系是单向 的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
- 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
以上的例子中,都是使用 A 类中的成员函数作为 B 类的友元,因此 A 可以访问 B 类的私有属性。其实加友元关键字后,该函数可以变成全局函数,这个全局函数写在类内(声明和实现都写在类内)。可以在其他地方直接调用(无需写作用域)。
class Person {
// 全局函数在类内实现, 加 friend 关键字
friend void showPerson(Person &p) {
cout << p.m_name << endl;
cout << p.m_age << endl;
};
public:
Person(string name, int age) {
this->m_name = name;
this->m_age = age;
};
private:
string m_name;
int m_age;
};
void test() {
Person p1("Tom", 25);
showPerson(p1); // 这是一个全局函数
}
int main() {
test();
system("pause");
}
4.5.5 运算符重载
运算符重载的概念:对已有的运算符进行定义,赋予其另一种功能,以适应不同的数据类型
4.5.5.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算。对于编译器内置的数据类型(如整型,浮点型),编译器知道如何进行加减乘除,但是对于自定义的数据类型,就不管用了。
成员函数实现 + 号重载
class Person {
public:
// 成员函数实现 + 号重载
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 test1() {
Person p1;
p1.m_A = 10;
p1.m_B = 20;
Person p2;
p2.m_A = 100;
p2.m_B = 200;
Person p3 = p1 + p2; //Person 类型的数据进行加号运算
cout << p3.m_A << endl;
cout << p3.m_B << endl;
}
int main() {
test1();
system("pause");
}
全局函数实现 + 号重载
class Person {...};
// 全局函数重载 + 号运算符
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;
}
运算符重载,也可以发生函数重载
class Person {...};
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 a) {
Person temp;
temp.m_A = p1.m_A + a;
temp.m_B = p1.m_B + a;
return temp;
}
void test1() {
Person p1;
Person p2;
Person p3 = p1 + p2; //Person 类型的数据进行加号运算
Person p4 = p1 + 10; //Person 类型的数据与整型数据进行加号运算
}
4.5.5.2 左移运算符重载
class Person {
public:
int m_A = 10 ;
int m_B = 20 ;
};
// 全局函数重载 << 号运算符
ostream & operator<<(ostream &out,Person &p) {
out << p.m_A << endl;
out << p.m_B << endl;
return out;
}
void test1() {
Person p1;
cout << p1 << endl; // 重载过的 << 运算符,可以直接输出 Person 类型的数据
cout << p1.m_A; // 原有的 int 类型数据也可以照样输出
}
int main() {
test1();
system("pause");
}
左移运算符只能在全局函数中重载。成员函数中达不到这个效果。
4.5.5.3 递增运算符重载
class Myinteger {
friend ostream& operator<<(ostream& out, Myinteger a);
public:
Myinteger() {
m_num = 0;
}
// 重载 ++ 运算符,前置 ++
Myinteger& operator++() {
m_num++;
return *this;
}
// 重载 ++ 运算符,后置 ++
Myinteger operator++(int) { //int 是占位参数,区分前置和后置递增
Myinteger temp = *this;
m_num++;
return temp; // 返回值,不能返回引用,temp 是一个局部(临时)变量
}
private:
int m_num ;
};
ostream& operator<<(ostream& out, Myinteger a) {
out << a.m_num;
return out;
}
void test1() {
Myinteger m;
cout << ++m << endl; // 结果 1
cout << m << endl; // 结果 1
Myinteger n;
cout << n++ <<endl; // 结果 0
cout << n << endl; // 结果 1
}
int main() {
test1();
system("pause");
}
重载后置 ++ 时,也可以将 temp 开辟在堆区,就可以返回引用了。
4.5.5.4 赋值运算符重载
C++ 编译器至少给一个类添加 4 个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。
class Person {
public:
Person(int age) {
m_A = new int(age); // 指针 m_A 指向堆区开辟的内存,内存存放 age 的值
}
~Person() {
if (m_A != NULL) {
delete m_A;
m_A = NULL;
}
}
// 重载赋值运算符
Person &operator=(Person &p) {
//m_A = p.m_A; 编译器提供的浅拷贝
if (m_A != NULL) { // 先判断是否有属性在堆区,先释放干净,再深拷贝
delete m_A;
m_A = NULL;
}
m_A = new int(*p.m_A); // 深拷贝
return *this;
}
int* m_A;
};
void test1() {
Person p1(20);
Person p2(18);
Person p3(30);
p2 = p1; // 采用自写的赋值深拷贝
p3 = p2 = p1; // 链式赋值,这就需要重载赋值时 return 本身
cout << *p1.m_A << endl;
cout << *p2.m_A << endl;
cout << *p3.m_A << endl;
}
int main() {
test1();
system("pause");
}
4.5.5.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
class Person {
public:
Person(string a, int b) {
m_Name = a;
m_Age = b;
}
bool operator==(Person& p) {
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
return true;
}
else {
return false;
}
}
string m_Name;
int m_Age;
};
void test1() {
Person p1("Tom", 25);
Person p2("Tom", 25);
if (p1 == p2) {
cout << "the same" << endl;
}
else {
cout << "the different" << endl;
}
}
int main() {
test1();
system("pause");
}
4.5.5.6 函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
void func(){
}
func() //func 是函数名,()是函数调用的意思
下面就是重载函数调用符括号():
class Myprint {
public:
void operator()(string test) {
cout << test << endl;
}
};
class Myadd {
public:
int operator()(int a, int b) {
return a + b;
}
};
void test1() {
Myprint myprint;
myprint("hello world");
Myadd add;
cout << add(10, 20) << endl;
cout << Myadd()(20, 30) << endl; // 匿名函数对象
}
int main() {
test1();
system("pause");
}
4.5.6 继承
继承的面向对象的三大特性之一。
有些类与类之间存在特殊的关系,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。
4.5.6.1 继承基本语法
语法:class 子类名:继承方式 父类{}
子类也叫派生类;父类也叫基类
// 定义父类
class BasePage {
public:
void header() {
cout << " 首页、公开课、登陆、注册……(公共头部)" << endl;
}
void footer() {
cout << " 帮助中心、交流合作、站内地图……(公共底部)" << endl;
}
private:
int date;
};
// 子类:Java 页面,继承 BasePage 类的属性
class Java : public BasePage{
public:
void content() {
cout << "JAVA 学科视频 " << endl;
}
};
// 子类:CPP 页面,继承 BasePage 类的属性
class CPP : public BasePage {
public:
void content() {
cout << "CPP 学科视频 " << endl;
}
};
void test1() {
Java java;
java.header();
java.footer();
java.content();
CPP cpp;
cpp.header();
cpp.footer();
cpp.content();
}
int main() {
test1();
system("pause");
}
4.5.6.2 继承方式
继承方式一共有三种:默认为,私有继承(private)。
- 公共继承(public)
- 保护继承(protected)
- 私有继承(private)
基类的私有属性永远都无法通过继承访问。
只能采用 friend 友元技术访问。
4.5.6.3 继承中的对象模型
从父类继承过来的成员,哪些成员继承到子类中了?
其实,父类中所有的非静态成员属性都会被子类继承下去,只是父类中的私有属性被编译器隐藏了,不可访问,但是确实继承下去了。
class BasePage {
public:
int a;
protected:
int b;
private:
int c;
};
class Son :public BasePage {
public:
int d;
};
void test1() {
Son s1;
cout << sizeof(s1) << endl; // 输出 16 (4*4)
}
int main() {
test1();
system("pause");
}
4.5.6.4 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数。那么父类和子类的构造和析构顺序是谁先谁后?
class BasePage {
public:
BasePage() {
cout << " 父类构造函数 " << endl;
}
~BasePage() {
cout << " 父类析构函数 " << endl;
}
};
class Son :public BasePage {
public:
Son() {
cout << " 子类构造函数 " << endl;
}
~Son() {
cout << " 子类析造函数 " << endl;
}
};
void test1() {
Son s1;
}
int main() {
test1();
system("pause");
}
/* 输出:
父类构造函数
子类构造函数
子类析造函数
父类析构函数
*/
4.5.6.5 继承中同名成员处理方式
继承中允许子类和父类有同名成员,不会覆盖。当子类与父类出现同名的成员,如何通过子类对象,访问到子类或者父类中同名的数据呢?
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
class BasePage {
public:
void func() {
cout << " 父类函数的调用 " << endl;
}
int m_A = 100;
int m_B = 200;
};
class Son :public BasePage {
public:
void func() {
cout << " 子类函数的调用 " << endl;
}
int m_A = 10;
};
void test1() {
Son s1;
cout << s1.m_A << endl; // 输出子类中自有的数据
cout << s1.BasePage::m_A << endl; // 输出父类中同名的数据
cout << s1.m_B << endl;
s1.func(); // 输出子类中自有的函数
s1.BasePage::func(); // 输出父类中同名的函数
}
int main() {
test1();
system("pause");
}
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问父类中同名函数。
4.5.6.6 继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象如何进行访问
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
class BasePage {
public:
static void func() {
cout << " 父类函数的调用 " << endl;
}
static int m_A;
int m_B = 200;
};
int BasePage::m_A = 100;
class Son :public BasePage {
public:
static void func() {
cout << " 子类函数的调用 " << endl;
}
static int m_A;
};
int Son::m_A = 10;
void test1() {
Son s1;
// 通过对象访问
cout << s1.m_A << endl; // 输出子类中自有的数据
cout << s1.BasePage::m_A << endl; // 输出父类中同名的数据
cout << s1.m_B << endl;
// 通过类名访问
cout << Son::m_A << endl; // 访问子类自己的属性
cout << Son::BasePage::m_A << endl; // 访问子类的父类属性
}
int main() {
test1();
system("pause");
}
4.5.6.7 多继承语法
C++ 允许一个类继承多个类。一个类继承了多个父类,称为多继承。
语法:class 子类 :继承方式 父类 1,继承方式 父类 2{}
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++ 实际开发中不建议用多继承
class Base1 {
public:
void func() {
cout << " 父类 1 函数的调用 " << endl;
}
int m_A = 100;
int m_B = 200;
};
class Base2 {
public:
void func() {
cout << " 父类 2 函数的调用 " << endl;
}
int m_A = 10;
int m_B = 20;
int m_C = 30;
};
class Son :public Base1,public Base2 {
public:
void func() {
cout << " 子类函数的调用 " << endl;
}
int m_A = 1;
int m_D = 2;
};
void test1() {
Son s1;
cout << s1.m_A << endl;
cout << s1.Base1::m_A << endl;
cout << s1.Base2::m_A << endl;
cout << s1.Base1::m_B << endl;
cout << s1.m_C << endl;
}
int main() {
test1();
system("pause");
}
4.5.6.8 菱形继承
菱形继承(钻石)概念:
- 两个派生类(B,C)继承同一个基类(A)
- 又有某个类(D)同时继承两个派生类
菱形继承的问题:D 类同时通过从 B/C 类继承了 A 的数据,也就是 D 类有两份 A 的数据,其实我们只需要一份就可以。
这就需要用到虚继承了,关键字 virtual
,解决内存浪费的问题。
class Animal {
public:
int m_A;
};
class Yang: virtual public Animal {};
class Tuo: virtual public Animal {};
class Son :public Yang,public Tuo {};
void test1() {
Son s1;
s1.Yang::m_A = 100;
s1.Tuo::m_A = 10;
cout << s1.Yang::m_A << endl; // 输出 10
cout << s1.Tuo::m_A << endl; // 输出 10
cout << s1.m_A << endl; // 输出 10
}
int main() {
test1();
system("pause");
}
若上述两个类 Yang
和Tuo
在继承 Animal
时不加 virtual
关键字,Son
类就会继承两份 m_A
,且不可用s1.m_A
访问到 m_A 数据。
函数在基类中被声明为 virtual
后,它在派生类中将自动成为虚方法。此时我们在派生类中将此方法声明不声明为 virtual
都没关系了,但是最好是声明出来好标记哪些方法是虚的。
4.5.6.9 链式继承
链式继承:C 继承自 B,B 继承自 A…
class Father {
public:
void printA() {
cout << " 父类中的方法 " << endl;
}
};
class Son:public Father{
public:
void printB() {
cout << " 子类中的方法 " << endl;
}
};
class GrandSon:public Son {
public:
void printC() {
cout << " 孙类中的方法 " << endl;
}
};
int main() {
GrandSon gs;
gs.printA(); // 输出“父类中的方法”
system("pause");
}
4.5.7 多态
4.5.7.1 多态的基本概念
多态是 C++ 面向对象三大特性之一,多态分为两类:
- 静态多态:函数重载和运算符重载属于静态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定—编译阶段确定函数地址
- 动态多态的函数地址晚绑定—运行阶段确定函数地址
静态多态:
class Animal {
public:
void speak() {
cout << " 动物在说话 " << endl;
}
};
class Cat:public Animal {
public:
void speak() {
cout << " 小猫在说话 " << endl;
}
};
// 地址早绑定,在编译阶段就确定函数的地址,传的是 animal 对象
void doSpeak(Animal &animal) {
animal.speak();
}
int main() {
Cat cat;
doSpeak(cat); // 输出动物说话
system("pause");
}
动态多态:
动态多态满足条件:
得有继承关系
子类要重写父类中的虚函数
重写:函数返回值,函数名称,参数列表完全相同
子类重写时,也可以是虚函数
class Animal {
public:
virtual void speak() { // 虚函数
cout << " 动物在说话 " << endl;
}
};
class Cat:public Animal {
public:
void speak() { // 普通成员函数
cout << " 小猫在说话 " << endl;
}
};
void doSpeak(Animal &animal) {
animal.speak(); //animal 的 speak 函数定义为虚函数,地址晚绑定
}
int main() {
Cat cat;
doSpeak(cat); // 输出小猫说话
Animal animal;
animal.speak("woo"); // 父类虚函数也可以直接调用
system("pause");
}
动态多态的使用:
- 父类的指针或者引用,执行子类对象 。如上例中,doSpeak() 传入的父类的引用,然后调用的时候,传的是子类对象。
再举一个链式继承中多态的例子:
class Father {
public:
virtual void print() {cout << " 父类中的方法 " << endl;}
};
class Son:public Father{
public:
virtual void print() {cout << " 子类中的方法 " << endl;}
};
class GrandSon:public Son {
public:
virtual void print() {cout << " 孙类中的方法 " << endl;}
};
int main() {
Son son;
Father *fa=&son;
fa->print();
GrandSon gs;
Father* faa = &gs;
faa->print();
system("pause");
}
4.5.7.2 多态案例
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
//--------- 普通类实现 -----------------
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;
}
// 这个函数里,如果想扩展新功能(如加入开方运算),需要修改源码
// 在真实开发环境中,提倡开闭原则
// 开闭原则:对扩展进行开发,对修改进行关闭
}
int m_num1;
int m_num2;
};
void test() {
Calculator cal;
cal.m_num1 = 10;
cal.m_num2 = 5;
cout << cal.getResult("*") << endl;
}
//---------- 利用多态实现 ----------------
class AbstractCalculator {
public:
virtual int getResult() {
return 0;
}
void doOther() {
cout << "some free" << endl;
}
int m_A; // 这两个必须是 public, 否则子类对象不可访问
int m_B;
};
class AddCalculator:public AbstractCalculator {
public:
int getResult() {
return m_A + m_B;
}
};
class SubCalculator :public AbstractCalculator {
public:
int getResult() {
return m_A - m_B;
}
};
class MultiCalculator:public AbstractCalculator {
public:
int getResult() {
return m_A * m_B;
}
};
void test2() {
// 多态的使用条件:父类指针或者引用指向子类对象
AbstractCalculator* abs = new AddCalculator; // 多态的调用
abs->m_A = 10;
abs->m_B = 5;
cout << abs->getResult() << endl;
delete abs;
abs = new SubCalculator;
abs->m_A = 20;
abs->m_B = 10;
cout << abs->getResult() << endl;
}
// 多态好处:组织结构清晰,可读性强,对于前期和后期扩展以及维护性高
int main() {
//test();
test2();
system("pause");
}
4.5.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。抽象类是类族的公共接口。
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回类型 函数名(参数列表)= 0;
这样不是纯虚函数:
virtual 返回类型 函数名(参数列表){return 0;}
当类中有了纯虚函数,这个类就称为抽象类。只要有一个纯虚函数就行,就是抽象类,就满足抽象类的特点。
抽象类特点:
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
//Base 为抽象类
class Base {
public:
virtual void func() = 0; // 纯虚函数
void doOther() {
cout << "free" << endl;
}
};
class Son :public Base {
public:
void func() { // 重写父类中的抽象类
cout << " 子类函数的调用 " << endl;
}
};
void test() {
//Base b1; 无法实例化,因为 Base 是抽象类
Base* base = new Son; // 父类指针或者引用指向子类对象
base->func();
}
int main() {
test();
system("pause");
}
在父类中写纯虚函数,就是为了在子类中重写这个函数。
4.5.7.4 多态案例
案例描述:制作饮品的大致流程为:煮水—冲泡—倒入杯中—加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
咖啡制作:煮水—冲泡咖啡—倒入杯中—加糖和牛奶
茶水制作:煮水—冲泡茶叶—倒入杯中—加柠檬
class AbstractDrinking {
public:
virtual void Boil() = 0; // 煮水
virtual void Brew() = 0; // 冲泡
virtual void PourInCup() = 0; // 倒入杯中
virtual void PutSomething() = 0; // 加入辅料
void makeDrinking() {
Boil();
Brew();
PourInCup();
PutSomething() ;
}
};
class MakeCoffee :public AbstractDrinking {
public:
void Boil() {
cout << " 煮纯净水 " << endl;
}
void Brew() {
cout << " 冲泡咖啡 " << endl;
}
void PourInCup() {
cout << " 将咖啡水倒入杯中 " << endl;
}
void PutSomething() {
cout << " 加糖和牛奶 " << endl;
}
};
class MakeTea :public AbstractDrinking {
public:
void Boil() {
cout << " 煮山泉水 " << endl;
}
void Brew() {
cout << " 冲泡茶叶 " << endl;
}
void PourInCup() {
cout << " 将茶水水倒入杯中 " << endl;
}
void PutSomething() {
cout << " 加柠檬 " << endl;
}
};
void test() {
AbstractDrinking* abs = new MakeCoffee;
abs->makeDrinking();
delete abs;
cout << "----------" << endl;
abs = new MakeTea;
abs->makeDrinking();
}
int main() {
test();
system("pause");
}
4.5.7.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~ 类名(){}
纯虚析构语法:
virtual ~ 类名()=0
在类中定义纯虚析构类名::~ 类名(){}
在全局中定义具体的实现
class Animal {
public:
Animal() {
cout << " 父类的构造函数的调用 " << endl;
}
// 利用虚析构可以解决 父类指针释放子类对象不干净的问题
virtual ~Animal() {
cout << " 父类的析构函数的调用 " << endl;
}
virtual void Speak() = 0;
};
class Cat: public Animal {
public:
Cat(string name) {
cout << " 子类构造函数的调用 " << endl;
m_Name = new string(name); // 堆区开辟内存
}
~Cat() {
if (m_Name != NULL) {
delete m_Name;
m_Name = NULL;
cout << " 子类析构函数的调用 " << endl;
}
}
void Speak() {
cout << *m_Name << " 小猫在说话 " << endl;
}
string* m_Name;
};
void test() {
Animal* animal = new Cat("Tom"); // 子类 Cat 开辟在堆区
animal->Speak();
delete animal; // 释放父类指针,父类含有虚析构,就可以走子类的析构
}
int main() {
test();
system("pause");
}
总结:
- 虚析构或纯虚构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。
- 拥有纯虚析构函数的类也属于抽象类
4.6 结构体与类
在C++
中,结构体 和类 它们都是有构造函数、析构函数和成员函数的,他们两者的根本区别就是:
结构体中访问控制默认是
public
的而类中默认的访问控制是
private
的。
示例一:
struct Mt {
int A ;
int B ;
int get_a() {
return A;
}
};
struct Mm :public Mt {int C = 5;};
int main() {
Mm mm;
mm.A = 10;
cout << mm.A << endl;
cout << mm.C << endl;
}
结构体成员函数也用 点. 获取。
示例二:
class Mt {
int A ;
int B ;
int get_a() {
return A;
}
};
struct Mm :public Mt {int C = 5;};
int main() {
Mm mm;
mm.A = 10; // 报错,无法访问,Mm 无法继承 Mt 的私有属性(默认私有)
}
类和结构体无明显区别,大多数情况下可以看成是一种东西。除了上述提到的默认访问控制问题。
4.7 文件操作
通过文件操作将数据持久化
C++ 中对文件操作需要包含头文件<fstream>
文件类型分为两种:
- 文本文件 — 文件以文本 ASCII 码形式存储在计算机中
- 二进制文件 — 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作,将 程序 数据输出到文件中
- ifstream:读操作,将文件中数据输入到 程序 中
- fstream:读写操作
4.7.1 文本文件
4.7.1.1 写文件
- 包含头文件
#<fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open('file_path', open_mode);
- 写数据
ofs<<'write txt';
- 关闭文件
ofs.close();
文件打开方式 open_mode:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用 | 操作符
如:ios::binary | ios::out
# include<iostream>
# include<string>
using namespace std;
# include<fstream>
void main(){
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "xxxxxxx" << endl;
ofs << "yyyyyyy" << endl;
ofs.close();
}
4.7.1.2 读文件
读文件步骤如下:
- 包含头文件
#<fstream>
- 创建流对象
ifstream ifs;
- 打开文件并判断文件是否打开成功
ifs.open('file_path', open_mode);
- 读数据
- 关闭文件
ofs.close();
# include<iostream>
# include<string>
using namespace std;
# include<fstream>
void main() {
ifstream ifs;
ifs.open("test.txt", ios::in);
if (ifs.is_open()) {
// 读数据
/* 方法 1
char buf[1024] = { 0 }; // 初始化一个数组
while (ifs >> buf) {
cout << buf << endl;
}
*/
/* 方法 2
char buf2[1024] = { 0 }; // 初始化一个数组
while (ifs.getline(buf2, sizeof(buf2))) {
cout << buf2 << endl;
};
*/
// 方法 3
string buf3;
while (getline(ifs, buf3)) {
cout << buf3 << endl;
}
/* 方法 4
char buf4;
while ((buf4 = ifs.get()) != EOF){
cout << buf4 << endl;
}
*/
ifs.close();
}
else {
cout << " 文件打开失败 " << endl;
}
}
4.7.2 二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为 ios::binary
4.7.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数 write
函数原型:ostream& write(const char* buffer, int len);
参数解释:字符型指针 buffer 指向内存中一段存储空间,len 是读写的字节数
4.7.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数 read
函数原型:istream& read(char* buffer, int len);
参数解释:字符型指针 buffer 指向内存中一段存储空间,len 是读写的字节数
# include<iostream>
# include<string>
using namespace std;
# include<fstream>
class Person {
public:
char m_name[64];
int age;
};
// 二进制文件的写入
void write() {
ofstream ofs("person.txt", ios::binary | ios::out);;
Person p = { " 张三 ", 18 };
ofs.write((const char*)&p, sizeof(Person)); //(const char*)强制转换成字符常量指针
ofs.close();
}
// 二进制文件的读取
void read() {
ifstream ifs("person.txt", ios::binary | ios::in);
if (ifs.is_open()) {
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << p.m_name << endl;
cout << p.age << endl;
ifs.close();
}
}
int main() {
write();
read();
}
五、实战 2- 职工管理系统
需求:职工管理系统可以用来管理公司内鄋员工的信息
本案例主要利用 C++ 来实现一个基于多态的职工管理系统
公司中职工分为三种,普通员工、经理、老板。显示信息时,需要显示职工编号,职工姓名、职工岗位、以及职责
普通员工职责:完成经理交给的任务
经理职责:完成老板交给的任务,并下发任务给员工
老板职责:管理公司所有事务
管理系统中需要实现的功能如下:
- 退出管理程序:退出当前管理系统
- 增加职工信息:实现批量添加职工功能,将信息录入到文件中,职工信息为:职工编号,姓名、部门编号
- 显示职工信息:显示公司内部所有职工的信息
- 删除离职员工:按照编号删除指定的职工
- 修改职工信息:按照编号修改职工个人信息
- 查找职工信息:按照编号或者姓名进行查找相关的人员信息
- 按照编号排序:按照职工编号,进行排序,排序规则由用户指定
- 清空所有文档:清空文件中所有职工信息(清空前需要再次确认,防止误删)
欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net