目录
字符串
C格式:char 变量名[] = "字符串值";
C++格式:string 变量名 = "字符串值"; 需加入头文件#include <string>
前置递增/递减,先运算后赋值。
后置递增/递减,先赋值后运算。
switch
switch中表达式类型只能是整型或字符型。
case中没有break,语句会一直向下执行。
switch语句虽然结构清晰,但是不能判断区间。
随机数
伪随机数:rand() % 100表示生成0~99的随机数42,但是每次生成的随机数都一样。
添加随机数种子,利用当前系统时间生成随机数,防止每次随机数都一样,添加头文件<ctime>。
随机数种子不放在循环中
#include <ctime>
srand((unsigned int)time(NULL));
int val = rand % 100;
数组
数组:存放相同类型数据元素的一个集合。
数组是由连续的内存位置组成的。
一维数组
三种定义方式:
- 数据类型 数组名[数组长度];
- 数据类型 数组名[数组长度] = {值1, 值2 ...};
- 数据类型 数组名[] = {值1, 值2 ...};
如果在初始化数据时,值的长度小于数组长度,会用0填充剩余数据部分。
两种数组名的用途:
1.通过数组名获取数组占用内存空间大小。
整个数组占内存空间大小:sizeof(arr_name)
单个元素占内存空间大小:sizeof(arr_name[0])
数组元素个数:sizeof(arr_name)/sizeof(arr_name[0])
2.通过数组名获取数组在内存中首地址。
数组首地址:(int)arr_name
数组中第i个元素地址:(int)&arr_name[i]
数组名是常量,不可以赋值。
二维数组
四种定义方式:
- 数据类型 数组名[行数][列数];
- 数据类型 数组名[行数][列数] = {{数据1, 数据2}, {数据3, 数据4}};
- 数据类型 数组名[行数][列数] = {数据1, 数据2, 数据3, 数据4};
- 数据类型 数组名[][列数] = {数据1, 数据2, 数据3, 数据4};
两种数组名的用途:
1.获取二维数组占内存空间大小:sizeof(arr_name)
第i行占内存空间大小:sizeof(arr_name[i])
第j列占内存空间大小:sizeof(arr_name[j])
某个元素占内存空间大小:sizeof(arr_name[i][j])
二维数组行数:sizeof(arr_name) / sizeof(arr_name[0])
二维数组列数:sizeof(arr_name[0]) / sizeof(arr_name[0][0])
2.获取二维数组首地址:(int)arr_name
第一行首地址:(int)arr_name[0]
第二行首地址:(int)arr_name[1]
第一个元素首地址:(int)&arr_name[0][0]
第二个元素首地址:(int)&arr_name[0][1]
函数
函数
组成:1.返回值类型2.函数名3.参数列表4.函数体语句5.return表达式
值传递的时候,形参发生任何改变,不会影响实参。
声明的作用:提前告诉编译器函数的存在,可以多次声明函数,但只能定义一次函数。
函数默认参数
如果某个位置有了默认参数,那么从这个位置往后,从左到右都必须有默认值。
如果函数声明有默认参数,那么函数定义不能有默认参数。如果函数定义有默认参数,那么函数声明不能有默认参数。函数声明和定义只能有一个默认参数。
函数占位参数
语法:返回值类型 函数名 (数据类型){}
占位参数可以有默认参数。如 void func(int = 10){}
函数重载
作用:函数名相同可以提高复用性。
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数的类型/个数/顺序不同
注意:
- 引用作为重载条件时,使用const区分引用和常量引用。
- 重载有默认参数时,出现二义性,报错,应避免这种情况。
//引用作为重载条件时
void func1(int &a)
{
cout << "func1 (int &a) 调用" << endl;
}
void func1(const int &a)
{
cout << "func1 (const int &a) 调用" << endl;
}
//重载有默认参数时
void func2(int a, int b = 10)
{
cout << "func2 (int a, int b = 10) 调用" << endl;
}
void func2(int)
{
cout << "func2 (int) 调用" << endl;
}
int main()
{
int a = 10;
func1(a); //输出 func1 (int &a) 调用
func1(10); //输出 func1 (const int &a) 调用
func2(a); //重载有默认参数,出现二义性,报错,应避免这种情况
return 0;
}
指针
指针
指针是一个变量,值为另一个变量的地址,即指针就是一个地址。
指针的定义:数据类型 * 指针变量名;
指针的赋值:指针变量名 = &指针指向的变量;
指针的使用:指针前加 * 表示解引用,找到指针指向的内存中的数据。
指针所占用的内存空间大小:任何数据类型的指针,在32位系统占用4字节,在64位系统占用8字节。
int main()
{
int a = 10;
//int *p = &a;
int *p;
p = &a;
cout << "a的地址" << &a << endl; //输出a的地址
cout << "指针p为:" << p << endl; //输出a的地址,和&a结果相同
cout << "a的值为:" << a << endl; //输出10
cout << "解引用*p为:" << *p << endl; //输出10
return 0;
}
空指针
指针变量指向内存中编号为0的空间。空指针用来初始化指针变量,如果指针一开始不知道指向哪里合适,就使用空指针。空指针指向的内存是不可访问的,因为0~255之间的内存编号是系统占用的,所以不可以访问。
int * p = NULL;
野指针
指针变量指向非法的内存空间。
const修饰指针
const修饰指针的三种情况:
1. const修饰指针 常量指针:指针的指向可以修改(可以指向另一个地址),指针指向的值不可以修改(*p不可以修改)。const int * p = &a; 常量(const)指针(*)
int main()
{
int a = 10;
int b = 20;
const int *p = &a;
cout << *p << endl;//输出10
//*p = 20; 错误,指针指向的值不可以修改
p = &b; //正确,指针指向可以修改
cout << *p << endl;//输出20
return 0;
}
2. const修饰常量 指针常量:指针的指向不可以修改(不可以指向另一个地址),指针指向的值可以修改(*p可以修改)。int * const p = &a; 指针(*) 常量(const)
int main()
{
int a = 10;
int b = 20;
int * const p = &a;
cout << *p << endl;//输出10
*p = 20;//正确,指针指向的值可以修改
//p = &b;错误,指针指向不可以修改
cout << *p << endl;//输出20
return 0;
}
3. const既修饰指针,又修饰常量:指针的指向和指针指向的值都不可以修改。const int * const p = &a;
指针和数组
变量的地址用&获取,数组的首地址是数组名本身
利用指针偏移遍历访问数组中元素:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int * p = arr; //arr为数组首地址
cout << "用指针访问第一个元素:" << *p << endl; //输出1
p++; //指针向后偏移4个字节
cout << "用指针访问第二个元素:" << *p << endl; //输出2
int * p2;
for (int i = 0; i < 10; i++) { //用指针遍历数组输出12345678910
cout << *p2 << endl;
p2++;
}
return 0;
}
指针和函数
利用指针做为函数的参数,可以修改实参的值。
使用值传递,不能修改实参。使用地址传递,可以修改实参。
将函数中的形参改为指针,可以减少内存空间,不会复制新的副本出来。
将函数中的形参改为指针,可以减少内存空间,不会复制新的副本出来。
将函数中的形参改为指针,可以减少内存空间,不会复制新的副本出来。
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
void swap2(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 10;
int b = 20;
//1.值传递,不能修改实参
swap1(a, b);
cout << a << " " << b << endl; //输出10 20
//2.地址传递,可以修改实参
swap2(&a, &b);
cout << a << " " << b << endl; //输出20 10
return 0;
}
结构体
结构体
结构体可以存储不同的数据类型。
语法:struct 结构体名 {成员列表};
三种方法创建结构体变量:
- struct 结构体名 变量名;
- struct 结构体名 变量名 = {成员列表中属性对应的值};
- struct 结构体名 {成员列表}变量名;
定义结构体时,关键字struct不能省略。
创建结构体变量时,关键字struct可以省略。
通过"."点号访问结构体变量中的属性
结构体数组
将自定义的结构体放入到数组中方便维护。结构体数组放在main函数中。
结构体数组做参数传递给函数时,形参表示为:struct 结构体名 结构体数组名[]
语法:struct 结构体名 数组名[元素个数] = { { 成员列表中属性对应的值 }, { }, ... { } };
//定义结构体
struct Student {
string name;
int age;
int score;
};
//遍历打印结构体数组
void printStudent(struct Student stuArray[], int len)
{
for (int i = 0; i < 3; i++) {
cout << "姓名:" << stuArray[i].name
<< " 年龄:" << stuArray[i].age
<< " 分数:" << stuArray[i].score << endl;
}
}
int main()
{
//创建结构体体数组
struct Student stuArray[3] = {
{"张三",18,100},
{"李四", 16,80},
{"王五", 14,60},
};
//给结构体数组中的元素赋值
stuArray[2].name = "赵六";
stuArray[2].age = 80;
stuArray[2].score = 50;
int len = sizeof(stuArray) / sizeof(stuArray[0]);
printStudent(stuArray, len);
return 0;
}
结构体指针
通过指针访问结构体中的成员,使用"->"访问结构体属性。
语法:
指针指向结构体变量:结构体名 * 指针变量名 = &结构体变量名;
指针访问结构体变量中数据:指针变量名->结构体属性名;
//定义结构体
struct Student {
string name;
int age;
int score;
};
int main()
{
//创建结构体变量
struct Student s = { "张三",15, 100 };
//通过指针指向结构体变量
Student * p = &s;
//通过指针访问结构体变量中的数据
cout << "姓名:" << p->name << "年龄:" <<
p->age << "分数:" << p->score << endl;
return 0;
}
结构体嵌套结构体
在一个结构体中定义另一个结构体成员。
//定义学生结构体
struct Student {
string name;
int age;
int score;
};
//定义老师结构体
struct teacher {
int id; //编号
string name; //姓名
int age; //年龄
struct Student stu; //学生
};
int main()
{
struct teacher t;
t.id = 150;
t.name = "老张";
t.age = 40;
t.stu.name = "张三";
t.stu.age = 20;
t.stu.score = 100;
cout << "老师编号" << t.id << "老师姓名:" << t.name <<
"老师年龄:" << t.age << "学生姓名:" << t.stu.name <<
"学生年龄:" << t.stu.age << "学生分数:" << t.stu.score << endl;
return 0;
}
结构体做函数参数
将结构体作为参数向函数中传递。传递方式:值传递/地址传递。
//打印学生信息函数
//值传递
void printstudent1(struct student s)
{
cout << "值传递 姓名:" << s.name << " 年龄:" << s.age << " 分数" << s.score << endl;
}
//地址传递
void printstudent2(struct student *p)
{
p->age = 50;
cout << "地址传递 姓名:" << p->name << " 年龄:" << p->age << " 分数" << p->score << endl;
}
int main()
{
//将学生传入到一个参数中,打印学生所有信息
struct student s;
s.name = "张三";
s.age = 20;
s.score = 100;
printstudent1(s); //值传递不修改实参(结构体属性值)
printstudent2(&s); //地址传递修改实参(结构体属性值)
cout << "姓名:" << s.name << " 年龄:" << s.age << " 分数" << s.score << endl;
return 0;
}
结构体中const使用
const防止误操作,设置成常量指针,如:const struct student *stu;
内存分区模型
内存分区模型
程序执行时,内存分为4个区域:代码区、全局区、栈区、堆区
程序运行前
在程序编译后生成exe可执行程序,未执行该程序前分为两个区域,代码区和全局区。
代码区
存放CPU执行的机器指令,由操作系统进行管理。代码区是共享的(对于被频繁执行的程序,只需在内存中有一份代码即可)、只读的(防止程序修改指令)。
全局区
存放全局变量(global)、静态变量(static)、常量(字符串常量、const修饰的全局变量)。全局区的数据在整个程序结束后由操作系统释放。
程序运行后
栈区
由编译器自动分配和释放,存放函数的形参值、局部变量等。
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。函数值执行完编译器自动释放,局部变量地址就不存在了,返回的地址不对应原来的局部变量。
堆区
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。主要使用new在堆区开辟内存。
int * func()
{
int * p = new int(10);
return p;
}
int main()
{
int * p = func();
cout << *p << endl;
return 0;
}
使用new关键字将数据开辟到堆区,指针本质也是局部变量,存放在栈区,但是指针保存的数据存放在堆区。
new操作符
使用new操作符在堆区开辟数据,使用delete操作符释放数据。
new创建的数据,会返回数据对应类型的指针。
如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。
用new分配数组空间时不能指定初值。
语法:
new 数据类型
new 数据类型 (初值)
new 数据类型 [数组大小]
delete操作符
语法:
delete 指针变量
delete [] 指针变量
int *p = new int(5); /* 开辟整型空间 */
int *p = new int;
*p = 5;
int p = *new int;
p = 5;
delete p; /* 释放空间 */
int *arr = new int[5]; /* 开辟数组空间 */
delete[] arr; /* 释放空间 */
引用
引用
作用:给变量起别名。
引用不创建新内存空间,指向同一块内存。
若修改变量别名的值,变量原名的值也会修改。
语法:数据类型 &别名 = 原名;
本质:指针常量,指针常量的指向不可以修改。int &b = a; 编译器自动转换为 int * const b = &a;
注意:
引用必须初始化。
引用初始化后不可以改变,可以赋值。
int main()
{
int a = 10;
int &b = a; //正确,引用初始化
int &b; //错误,没有初始化
//int &b = 10; //错误,引用必须引一块合法的内存空间
//正确,加上const后编译器将代码修改为 int temp = 10; const int &b = temp;
const int &b = 10;
int c = 20;
b = c; //正确,赋值操作,不是更改引用
int &b = c; //错误,初始化后不可以改变
return 0;
}
引用做函数参数
作用:函数传参时,引用让形参修饰实参。可以简化指针修改实参。
//值传递
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
//地址传递
void swap2(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//引用传递
void swap3(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 10;
int b = 20;
//swap1(a, b);//值传递,形参不会修饰实参 => a=10 b=20
//swap2(&a, &b);//地址传递,形参会修饰实参 => a=20 b=10
swap3(a, b);//引用传递,形参会修饰实参 => a=20 b=10
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
引用做函数返回值
注意:不要返回局部变量引用。如果函数的返回值是引用,则函数调用可以作为左值。
//不要返回局部变量的引用
int & test1()
{
int a = 10; //局部变量存放在栈区
return a;
}
//函数的调用可以作为左值
int & test2()
{
//静态变量存放在全局区,全局区上的数据在整个程序结束后系统释放
static int a = 10;
return a;
}
int main()
{
//int &ref = test1();
//
//cout << "ref = " << ref << endl;//输出10,因为编译器做了保留
//cout << "ref = " << ref << endl;//输出乱码,因为a内存被释放
int &ref2 = test2();
cout << "ref2 = " << ref2 << endl;//输出10
cout << "ref2 = " << ref2 << endl;//输出10
test2() = 20; //如果函数的返回值是引用,则函数调用可以作为左值
cout << "ref2 = " << ref2 << endl;//输出20
cout << "ref2 = " << ref2 << endl;//输出20
return 0;
}
常量引用
作用:常量引用用来修饰形参,防止误操作。在形参列表中,加const修饰形参,防止形参改变实参。
//打印数据函数,修饰形参防止误操作
void print_val(const int &val)
{
//val = 200;
cout << "val = " << val << endl;
}
int main()
{
//int &b = 10; //错误,引用必须引一块合法的内存空间
const int &b = 10; //正确,加上const后编译器将代码修改为 int temp = 10; const int &b = temp;
int a = 10;
print_val(a); //输出10
return 0;
}
类和对象
C++面向对象三大特性:封装、继承、多态。
C++认为所有事物都为对象,对象上有属性和行为。
封装
意义:
- 将属性和行为作为一个整体。
- 将属性和行为加以权限控制。
1.将属性和行为作为一个整体。
语法:class 类名{ 访问权限: 属性 / 行为 };
实例化:通过一个类创建一个对象的过程。
成员:类中的属性和行为的统称。
属性:成员属性、成员变量
行为:成员函数、成员方法
2.将属性和行为加以权限控制。
访问权限三种:
- public 公共权限 类内可以访问 类外可以访问
- protected 保护权限 类内可以访问 类外不可以访问 子类可以访问父类
- private 私有权限 类内可以访问 类外不可以访问 子类不可以访问父类
struct和class的区别
默认的访问权限不同。
- struct 默认权限为 公共
- class 默认权限为 私有
对象的初始化和清理
构造函数和析构函数
如果不提供构造函数和析构函数,编译器会提供,编译器提供的构造函数和析构函数是空实现。
构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用。
语法:类名(){}
- 没有返回值,不写void
- 函数名称与类名相同
- 可以有参数,因此可以发生重载
- 程序调用对象时会自动调用构造函数,无需手动调用,且只会调用一次
析构函数:销毁对象前系统自动调用,执行清理工作。
语法:~类名(){}
- 没有返回值,不写void
- 函数名称与类名相同,在名称前加上波浪号~
- 没有参数,不可以发生重载
- 程序在对象销毁前会自动调用析构函数,无需手动调用,且只会调用一次
构造函数的分类及调用
分类方式:
- 按参数分:有参构造和无参构造(默认)
- 按类型分:普通构造和拷贝构造
拷贝构造函数调用时机:
- 使用一个已经创建完毕的对象初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
调用方式:
- 括号法
- 显示法
- 隐式转换法
默认情况下编译器至少给一个类添加三个函数:
- 默认构造函数(无参数,函数体为空)
- 默认析构函数(无参数,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则:
- 如果用户定义有参构造函数,编译器不提供默认无参构造函数,但会提供默认拷贝构造函数。
- 如果用户定义拷贝构造函数,编译器不提供其他构造函数。
class Person
{
public:
//无参构造函数(默认)
Person()
{
cout << "Person无参构造函数" << endl;
}
//有参构造函数
Person(int a)
{
age = a;
cout << "Person有参构造函数" << endl;
}
//拷贝构造函数
Person(const Person &p)
{
//将传入的人类上所有属性拷贝进来
age = p.age;
cout << "Person拷贝构造函数" << endl;
}
//析构函数
~Person()
{
cout << "Person析构函数" << endl;
}
int age;
};
//构造函数调用
void test()
{
//括号法
Person p1; //默认构造函数调用
Person p2(10); //有参构造函数调用
Person p3(p2); //拷贝构造函数调用
//注意:调用无参(默认)构造函数时,不加(),否则编译器会语句认为是函数的声明。
//显示法
Person p1; //默认构造函数调用
Person p2 = Person(10); //有参构造函数调用
Person p3 = Person(p2); //拷贝构造函数调用
Person(10); //匿名对象,当前行执行结束后,系统会立即回收匿名对象
//注意:不要用拷贝构造函数初始化匿名对象,否则编译器会认为Person(p2) ===Person p2;
//隐式转换法
Person p4 = 10; //相当于 Person p4 = Person(10); 有参构造函数调用
Person p5 = p4; //拷贝构造函数调用
}
int main()
{
test();
return 0;
}
深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作。会造成堆区内存重复释放。
深拷贝:在堆区用new重新申请空间,进行拷贝操作。
如果属性在堆区开辟的,要自己提供拷贝构造函数,防止浅拷贝带来的问题
初始化列表
作用:用来初始化属性。
语法:构造函数(): 属性1(值1), 属性2(值2)...{}
class Person
{
public:
Person():m_a(10),m_b(20),m_c(30){}
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;
};
void test()
{
Person p;
Person p(10, 20, 30);
}
类对象作为类成员
对象成员:类中的成员是另一个类的对象。
当其他类对象作为本类成员时,构造的时候先构造其他类的对象,再构造自身;析构顺序与构造相反(析构的时候先析构自身类的对象,再析构其他类的对象)。
静态成员
静态成员:在成员变量和成员函数前加上关键字static
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 静态成员函数可以设置访问权限,类外访问不到私有静态成员函数
class Person
{
public:
static void func() //静态成员函数
{
m_a = 100; //静态成员函数可以访问静态成员变量
//m_b = 200; //错误静态成员函数不可以访问非静态成员变量
cout << "static void func 调用" << endl;
}
static int m_a; //静态成员变量
int m_b;
private:
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_a = 100;
void test()
{
//通过对象访问
Person p;
p.func();
//通过类名访问
Person::func();
//Person::func2();//私有静态成员函数访问不了
}
int main()
{
test();
return 0;
}
对象模型
类内的成员变量和成员函数分开存储。只有非静态成员变量才属于类的对象。静态成员变量、静态成员函数、非静态成员函数不属于类对象。
空对象占用内存空间大小:1字节。C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置。每个空对象有独一无二的内存地址。
this指针
this指针指向被调用的成员函数所属的对象。谁调用它就指向谁。
this指针本质:指针常量,指针指向不可以修改。
this指针是隐含每一个非静态成员函数内的一种指针。
this指针不需要定义,直接使用即可。
用途:
- 当形参和成员变量同名是,用this指针来区分。
- 在类的非静态成员函数中返回对象本身,可使用 return *this;
class Person
{
public:
Person(int age)
{
//this指针指向被调用的成员函数所属的对象p1
this->age = age;
}
Person & person_add_age(Person &p)
{
this->age += p.age;
//this指向p2的指针,*this指向p2对象本身
return *this;
}
int age;
};
void test1()
{
Person p1(18);
cout << "p1年龄为:" << p1.age << endl;
}
void test2()
{
Person p1(10);
Person p2(10);
//链式编程
p2.person_add_age(p1).person_add_age(p1).person_add_age(p1);
cout << "p2年龄为:" << p2.age << endl;
}
int main()
{
//test1();
test2();
return 0;
}
空指针访问成员函数
空指针可以调用成员函数,但是要注意有没有用到this指针。如果用到this指针,需要加以判断,保证代码健壮性。
if (this == NULL) {
return;
}
const修饰成员函数
常函数:
- 成员函数后加const的函数,修饰的是this指向,让指针指向的值不可以修改
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable,在常函数中可以修改变量值
常对象:
- 声明对象前加const的对象
- 常对象只能调用常函数
class Person
{
public:
//this指针本质是指针常量,指针指向不可修改
//Person * const this; 成员函数后 不加const时this指针表示形式
//const Person * const this; 成员函数后 加const时this指针表示形式
void show_person() const
{
//this->m_a = 100;
//this = NULL;//不可以修改指针指向
this->m_b = 100;
}
void func(){ }
int m_a;
mutable int m_b;//特殊变量,在常函数中可以修改这个值
};
void test1()
{
Person p;
p.show_person();
}
void test2()
{
const Person p; //常对象
//p.m_a = 100; //不可以修改
p.m_b = 100; //可以修改,有mutable修饰
//p.func(); //常对象不能调用普通成员函数
p.show_person(); //常对象只能调用常函数
}
int main()
{
test1();
test2();
return 0;
}
友元
友元:让一个函数或类访问另一个类中的私有成员。
关键字:friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
在有私有成员的类中声明全局函数并在前加friend,就可以访问类中的私有成员
类做友元
在有私有成员的类中声明来访问的类并在前加friend,如 friend class Person;
成员函数做友元
在有私有成员的类中声明来成员函数类并在前加friend,如 friend void Person::visit();
运算符重载
对已有的运算符重新定义,赋予另一种功能,适应不同数据类型。
加号运算符重载
实现两个自定义数据类型相加。对于内置的数据类型的表达式的运算符不可修改。不要滥用运算符重载。
- 成员函数重载+号
- 全局函数重载+号
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;
};
//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 test()
{
Person p1;
p1.m_a = 10;
p1.m_b = 10;
Person p2;
p2.m_a = 10;
p2.m_b = 10;
//成员函数重载本质调用
//Person p3 = p1.operator+(p2);
//全局函数重载本质调用
//Person p3 = operator+(p1, p2);
Person p3 = p1 + p2;
cout << "p3.m_a = " << p3.m_a << endl;
cout << "p3.m_b = " << p3.m_b << endl;
Person p4 = p1 + 20; //Person + int
cout << "p4.m_a = " << p4.m_a << endl;
cout << "p4.m_b = " << p4.m_b << endl;
}
int main()
{
test();
return 0;
}
左移运算符重载
输出自定义数据类型。重载左移运算符配合友元可以实现输出私有自定义数据类型
- 全局函数重载左移运算符
class Person
{
friend ostream & operator<<(ostream &cout, Person &p);
public:
Person(int a, int b)
{
m_a = a;
m_b = b;
}
private:
//利用成员函数重载左移运算符 p.operator<<(cout) 简化为 p << cout
//不会利用成员函数重载左移运算符,因为无法实现const在左侧
//void operator<<(cout)
int m_a;
int m_b;
};
//全局函数重载左移运算符
ostream & operator<<(ostream &cout, Person &p) //operator<<(cout, p) 简化为 cout << p
{
cout << "p.m_a = " << p.m_a << "p.m_b = " << p.m_b;
return cout;
}
void test()
{
Person p(10,10);
cout << p << endl;
}
int main()
{
test();
return 0;
}
递增运算符重载
class MyInteger
{
friend ostream& operator<<(ostream& cout, MyInteger myint);
public:
MyInteger()
{
m_num = 0;
}
//重载前置++运算符
MyInteger& operator++()
{
m_num++;
return *this; //将自身返回
}
//重载后置++运算符
MyInteger& operator++(int) //int表示占位参数,区分前置和后置
{
//先记录当前结果,后递增,最后将记录结果返回
MyInteger temp = *this;
m_num++;
return temp;
}
private:
int m_num;
};
//重载<<运算符
ostream& operator<<(ostream& cout, MyInteger myint)
{
cout << myint.m_num;
return cout;
}
void test1()
{
MyInteger myint;
cout << ++(++myint) << endl;
cout << myint << endl;
}
void test2()
{
MyInteger myint;
cout << myint++ << endl;
cout << myint << endl;
}
int main()
{
//test1();
test2();
return 0;
}
赋值运算符重载
C++编译器至少给一个类添加4个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符operator=,对属性进行值拷贝
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)
{
//先判断是否有属性在堆区,如果有先释放干净,再深拷贝
if (m_age != NULL) {
delete m_age;
m_age = NULL;
}
m_age = new int(*p.m_age); //深拷贝
return *this;
}
int *m_age;
};
void test1()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; //赋值操作
cout << "p1年龄:" << *p1.m_age << endl;
cout << "p2年龄:" << *p2.m_age << endl;
cout << "p3年龄:" << *p3.m_age << endl;
}
int main()
{
test1();
return 0;
}
关系运算符重载
class Person
{
public:
Person(string name, int age)
{
m_name = name;
m_age = age;
}
//重载==号
bool operator==(Person &p)
{
if (this->m_name == p.m_name && this->m_age == p.m_age) {
return true;
}
return false;
}
bool operator!=(Person &p)
{
if (this->m_name == p.m_name && this->m_age == p.m_age) {
return false;
}
return true;
}
string m_name;
int m_age;
};
void test()
{
Person p1("tom", 18);
Person p2("jerry", 18);
if (p1 == p2) {
cout << "p1 p2 相等" << endl;
}
else {
cout << "p1 p2 不相等" << endl;
}
if (p1 != p2) {
cout << "p1 p2 不相等" << endl;
}
else {
cout << "p1 p2 相等" << endl;
}
}
int main()
{
test();
return 0;
}
函数调用运算符重载
调用运算符为()
重载后使用的方式像函数的调用,称为仿函数。仿函数无固定写法。
class MyPrint
{
public:
//重载函数调用运算符
void operator()(string test)
{
cout << test << endl;
}
};
void myprint2(string test)
{
cout << test << endl;
}
void test1()
{
MyPrint myprint;
myprint("hello world"); //仿函数
myprint2("hellor world");
}
//仿函数无固定写法
class MyAdd
{
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};
void test2()
{
MyAdd myadd;
int ret = myadd(100, 100);
cout << "ret = " << ret << endl;
//匿名函数对象
cout << MyAdd()(100, 100) << endl;
}
int main()
{
test1();
test2();
return 0;
}
继承
语法: class 子类 : 继承方式 父类
子类(派生类)/ 父类(基类)
派生类中包括两部分:1.从基类继承过来 2.自己增加的成员
继承方式:公共继承、保护继承、私有继承
父类中所有非静态成员属性都会被子类继承下去。
父类中的私有成员只是被编译器隐藏了,虽然访问不到,但是还会继承下去。
公共继承:
- 父类中的公共权限成员,到子类中依然是公共权限,类外可以访问
- 父类中的保护权限成员,到子类中依然是保护权限,类外访问不到
- 父类中的私有权限成员,子类访问不到
保护继承:
- 父类中的公共权限成员,到子类中变成保护权限,类外访问不到
- 父类中的保护权限成员,到子类中变成保护权限,类外访问不到
- 父类中的私有权限成员,子类访问不到
私有继承:
- 父类中的公共权限成员,到子类中变成私有权限,类外访问不到
- 父类中的保护权限成员,到子类中变成私有权限,类外访问不到
- 父类中的私有权限成员,子类访问不到
class A
{
public:
int a;
protected:
int b;
private:
int c;
};
//共有继承
class B : public A
{
public:
int a;
protected:
int b;
//int c; 无法访问
};
//保护继承
class B :protected A
{
protected:
int a;
int b;
//int c; 无法访问
};
//私有继承
class B :private A
{
private:
int a;
int b;
//int c; 无法访问
};
继承中构造函数和析构函数顺序
先构造父类,再构造子类。析构顺序与构造顺序相反。
父类构造函数-->子类构造函数-->子类析构函数-->父类析构函数
继承同名成员处理方式
- 访问子类同名成员,直接访问。
- 访问父类同名成员,需要加作用域。
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问父类中同名函数。
class Base
{
public:
Base()
{
m_a = 100;
}
void func()
{
cout << "Base - fun()" << endl;
}
void func(int a)
{
cout << "Base - fun(int a)" << endl;
}
int m_a;
};
class Son :public Base
{
public:
Son()
{
m_a = 200;
}
void func()
{
cout << "Son - fun()" << endl;
}
void func(int a)
{
cout << "Son - fun(int a)" << endl;
}
int m_a;
};
//同名成员属性
void test1()
{
Son s1;
cout << "Son下 m_a = " << s1.m_a << endl;
//子类访问父类中同名成员,需要添加作用域
cout << "Base下 m_a = " << s1.Base::m_a << endl;
}
//同名成员函数
void test2()
{
Son s2;
s2.func();
//子类访问父类中同名成员函数,需要添加作用域
s2.Base::func();
s2.Base::func(100);
}
int main()
{
test1();
test2();
return 0;
}
继承同名静态成员处理方式
- 访问子类同名成员,直接访问。
- 访问父类同名成员,需要加作用域。
- 同盟静态成员处理方式和非静态处理方式一样,只不过有两种访问方式(通过对象和通过类名)。
class Base
{
public:
static int m_a;
static void func()
{
cout << "Base - static void func()" << endl;
}
};
int Base::m_a = 100;
class Son : public Base
{
public:
static int m_a;
static void func()
{
cout << "Son - static void func()" << endl;
}
};
int Son::m_a = 200;
//同名静态成员属性
void test1()
{
//1.通过对象访问
cout << "通过对象访问:" << endl;
Son s1;
cout << "Son 下 m_a = " << s1.m_a << endl;
cout << "Base 下 m_a = " << s1.Base::m_a << endl;
//2.通过类名访问
cout << "通过类名访问:" << endl;
cout << "Son 下 m_a = " << Son::m_a << endl;
//第一个::表示通过类名方式访问,第二个::表示访问父类作用域
cout << "Base 下 m_a = " << Son::Base::m_a << endl;
}
//同名静态成员函数
void test2()
{
//1.通过对象访问
cout << "通过对象访问:" << endl;
Son s2;
s2.func();
s2.Base::func();
//2.通过类名访问
cout << "通过类名访问:" << endl;
Son::func();
Son::Base::func();
}
int main()
{
test1();
test2();
return 0;
}
多继承
语法:class 子类 : 继承方式 父类1, 继承方式 父类2 ...
菱形继承
两个派生类继承同一个基类,又有某个类同时继承两个派生类。
当两个父类拥有相同数据,需要加作用域区分。菱形继承导致数据有两份,造成资源浪费。
利用虚继承解决菱形继承问题,在继承之前加上关键字virtual,Base类称为虚基类。
虚基类指针- vbptr(v - virtual b - base ptr - pointer)指向vbtable虚基类表
多态
多态通常用指针或引用实现
静态多态:函数重载和运算符重载属于静态多态,复用函数名。
动态多态:派生类和虚函数实现时多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定,编译阶段确定函数地址。
- 动态多态的函数地址晚绑定,运行阶段确定函数地址。
重写:函数返回值类型、函数名、参数列表完全相同
只要父类在定义成员函数时声明了virtual关键字,在子类中实现的时候覆盖该函数时,virtual关键字可加可不加,不影响多态的实现。
多态满足条件:
- 有继承关系
- 子类重写父类的虚函数,子类的虚函数表内部会替换成子类的虚函数地址。
多态使用条件:
- 父类的指针或引用指向子类对象
虚函数指针 - vfptr(v - virtual f - function ptr - pointer)指向vftable虚函数表,表内记录虚函数的地址。
多态优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展和维护
//普通写法实现计算器
class Calculator
{
public:
int get_result(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 test1()
{
Calculator c;
c.m_num1 = 10;
c.m_num2 = 10;
cout << c.m_num1 << " + " << c.m_num2 << " = " << c.get_result("+") << endl;
cout << c.m_num1 << " - " << c.m_num2 << " = " << c.get_result("-") << endl;
cout << c.m_num1 << " * " << c.m_num2 << " = " << c.get_result("*") << endl;
}
//多态写法实现计算器
//计算器抽象类
class AbstractCalculator
{
public:
virtual int get_result()
{
return 0;
}
int m_num1;
int m_num2;
};
//加法计算器类
class AddCalcultor :public AbstractCalculator
{
public:
int get_result()
{
return m_num1 + m_num2;
}
};
//减法计算器类
class SubCalcultor :public AbstractCalculator
{
public:
int get_result()
{
return m_num1 - m_num2;
}
};
//乘法计算器类
class MulCalcultor :public AbstractCalculator
{
public:
int get_result()
{
return m_num1 * m_num2;
}
};
void test2()
{
//多态使用条件:父类指针或引用指向子类对象
//加法运算
AbstractCalculator * abc = new AddCalcultor;
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " + " << abc->m_num2 << " = " << abc->get_result() << endl;
delete abc; //用完销毁
//减法运算
abc = new SubCalcultor; //虽然销毁,但是指针类型没有变,还是父类指针
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " - " << abc->m_num2 << " = " << abc->get_result() << endl;
delete abc;
//乘法运算
abc = new MulCalcultor;
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " * " << abc->m_num2 << " = " << abc->get_result() << endl;
delete abc;
}
int main()
{
test1();
test2();
return 0;
}
纯虚函数和抽象类
在多态中,通常父类中的虚函数实现是毫无意义的,主要是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
语法:virtual 返回值类型 函数名 (参数列表) = 0;
抽象类:有纯虚函数的类。
抽象类特点:
- 无法实例化对象。
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0; 类名::~类名(){}
虚析构和纯虚析构用来解决通过父类指针释放子类对象
如果子类中没有堆区数据,可以不写虚析构或纯虚析构
文件操作
文件操作头文件<fstream>
文件操作:
- 文本文件
- 二进制文件
操作文件三大类:
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
文本文件
文件以文本的ASCII码形式存储
写文件
写文件步骤:
- 包含头文件 #include <fstream>
- 创建流对象 ofstream ofs;
- 打开文件 ofs.open("文件路径", 打开方式);
- 写数据 ofs << "写入的数据";
- 关闭文件 ofs.close();
文件打开方式:打开方式可以互相配合使用,使用 | 操作符
- ios::in 为读文件而打开文件
- ios::out 为写文件而打开文件
- ios::ate 初始位置:文件结尾
- ios::app 追加方式写文件
- ios::trunc 如果文件存在先删除,再创建
- ios::binary 二进制方式
读文件
读文件步骤:
- 包含头文件 #include <fstream>
- 创建流对象 ifstream ifs;
- 打开文件并判断是否打开成功 ifs.open("文件路径", 打开方式);
- 读数据 四种方式读取
- 关闭文件 ifs.close();
ifs.is_open() 判断打开成功返回1,打开失败返回0
void test()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open()) {
cout << "failed to open" << endl;
return;
}
//读数据
//第一种
char buf[1024] = { 0 };
while (ifs >> buf) {
cout << buf << endl;
}
//第二种
char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf))) {
cout << buf << endl;
}
//第三种
string buf;
while (getline(ifs, buf)) {
cout << buf << endl;
}
//第四种
char c;
while ((c = ifs.get()) != EOF) {
cout << c;
}
ifs.close();
}
二进制文件
文件以文本的二进制形式存储,打开方式指定ios::binary
写文件
调用成员函数write
函数原型:ostream& write(const char * buffer, int len);
字符指针buffer指向内存中一段存储空间,len是读写字节数。
class Person
{
public:
char m_name[64];
int m_age;
};
void test()
{
ofstream ofs("person.txt", ios::out | ios::binary);
Person p = { "haha", 20 };
ofs.write((const char *)&p, sizeof(p));
ofs.close();
}
读文件
调用成员函数read
函数原型:istream& read(char * buffer, int len);
字符指针buffer指向内存中一段存储空间,len是读写字节数。
模板
模板目的:提高复用性,将类型参数化。
函数模板
建立一个通用函数,函数返回值类型和形参类型不具体制定,用一个虚拟的类型代表
语法:
- template<typename T>
- 函数声明或定义
- template 声明创建模板
- typename 表明后面的符号是一种数据类型,可以用class代替,typename和class意义一样
- T 通用的数据类型,名称可以替换,通常为大写字母
两种方式使用函数模板:
- 自动类型推导
- 显示指定类型(建议使用)
使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型。
/*
* 函数模板
* 声明一个模板,告诉编译器后面代码中T不要报错,T是一个通用数据类型
*/
template<typename T>
void my_swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
void test()
{
int a = 10;
int b = 20;
//两种方式使用函数模板
//1.自动类型推导
//my_swap(a, b);
//2.显示指定类型
my_swap<int>(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
int main()
{
test();
return 0;
}
普通函数与函数模板区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
普通函数和函数模板调用规则:
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板,如test<>(a,b);
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
类模板
建立一个通用类,类中得成员数据类型可以不具体指定,用一个虚拟的类型代表。
语法:
- template<typename T1, typename T2, typename T3 ....>
- 类
- template 声明创建模板
- typename 表明后面的符号是一种数据类型,可以用class代替,typename和class意义一样
- T 通用的数据类型,名称可以替换,通常为大写字母
类模板中成员函数创建时机
类模板中成员函数和普通类中成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
类模板与函数模板区别
类模板与函数模板区别主要有两点:
- 类模板没有自动类型推导的使用方式,如Person<string, int>p("haha",20);
- 类模板在模板参数列表中可以有默认参数,如template<typename T=int>
类模板与继承
当类模板碰到继承时,需要注意一几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
类模板成员函数类外实现
类模板中成员函数类外实现时,需要加上模板参数列表
template<class T1, class T2>
class Person
{
public:
//成员函数类内声明
Person(T1 name, T2 age);
void show_person();
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>::show_person()
{
cout << "姓名:" << this->m_name << "年龄:" << this->m_age << endl;
}
类模板分文件编写
将声明和实现写到同一个文件中,并更改后缀名为.hpp
类模板与友元
- 全局函数类内实现-直接在类内声明友元即可
- 全局函数类外实现-需要提前让编译器知道全局函数的存在
//提前让编译器知道Person类的存在
template<class T1, class T2>
class Person;
//类外实现-提前让编译器知道全局函数的存在
template<class T1, class T2>
void print_person_2(Person<T1, T2> p)
{
cout << "类外实现 姓名:" << p.m_name << " 年龄:" << p.m_age << endl;
}
template<class T1, class T2>
class Person
{
//全局函数 类内实现
friend void print_person_1(Person<T1,T2> p)
{
cout << "姓名:" << p.m_name << " 年龄:" << p.m_age << endl;
}
//全局函数 类外实现
//加空模板参数列表
//全局函数类外实现 - 需要提前让编译器知道全局函数的存在
friend void print_person_2<>(Person<T1, T2> p);
public:
Person(T1 name, T2 age)
{
this->m_name = name;
this->m_age = age;
}
private:
T1 m_name;
T2 m_age;
};
void test()
{
Person<string, int>p("haha", 20);
print_person_1(p);
print_person_2(p);
}
int main()
{
test();
return 0;
}