1.字符型变量的使用
2.字符串定义
c语言风格:类型 + 数组名[ ] ,例如 int arr[10] = "abcdefg"
c++风格:string + 数组名,例如 string arr = "abcdefg"
3.new的使用
使用样例:
struct Person {
int age;
};
int main() {
struct Person* pa = new struct Person;
//new开辟了一块struct Person大小的空间
//如何返回该地址,所以用struct Person* 类型接收
delete pa;
return 0;
}
注意!!!
new int[10]和new int(10)有什么区别?
总结:可以看出,new int[10]
和 new int(10)
分别用于分配数组和单个对象,并且它们的释放方式也不同。因此在使用时需要根据实际需求进行选择,同时也需要注意动态内存的生命周期和释放问题。
3.1 new,delect和malloc,free的区别
注意:
1.返回类型编译器会自行判断,不用像malloc一样手动强制类型转换
2.new开辟失败会报错异常,所以不用像malloc那样检查是否为空指针
4.引用 &
4.1基本使用
4.2注意事项
一个名字不能同时成为两块内存的别名,但一块内存可以有多个别名
不能错误初始化
4.3 引用&在函数中使用
int& test1()
{
static int a = 1000;
return a;
}
int main()
{
int& ref = test1();
cout << ref << endl;
//函数可以做左值
test1() = 2000;
cout << ref << endl;
return 0;
}
4.4 引用的本质
引用的本质就是一个指针常量
且一旦初始化后,就不可以发生改变,因为有&的内层含义里有const
看了以下解释有点晕,但其实不用想的太复杂,
如果 int& ref = a,那么就把 ref 完完全全看作 a 就好了(包括地址),因为ref永远都指向a的空间
所以如果ref改变,那么这块内存中存储的值也改变,也就是a也改变
4.5 引用常量的使用情况
因为是引用(相当于传址),所以如果在函数里修改了值,函数外的实参也会被一同修改,为了防止误操作,就加上const
5.函数默认参数
在函数中,如果我们自己传入数据,就用自己的数据,如果没有,则用我们设置的默认值
注意事项1:如果有个位置有了默认参数,那么这个位置往后,都要设置默认值
正确写法:
int test(int a, int b = 10, int c = 30)
{
return a + b;
}
错误写法:
int test(int a, int b = 10, int c)
{
return a + b;
}
注意事项2:如果函数声明有默认参数,函数实现就不能有默认参数
正确写法:
int test(int a = 20, int b = 30);
int test(int a, int b )
{
return a + b;
}
或
int test(int a, int b);
int test(int a = 20, int b = 30 )
{
return a + b;
}
错误写法:
int test(int a = 10, int b = 20);
int test(int a = 20, int b = 30 )
{
return a + b;
}
6.函数占位参数
7.函数重载
函数参数类型不同
int test(int a, int b)
{
return a + b;
}
double test(int a, double b)
{
return a + b;
}
int main()
{
cout << test(1, 2) << endl;
cout << test(1,2.3) << endl;
return 0;
}
函数参数顺序不同
函数参数数量不同
注意1:函数返回值不可以作为函数重载的条件
注意2:函数重载碰到默认参数,出现二义性
注意3:当引用作为重载条件
8.封装
8.1 封装意义1:
一个类的组成 = 属性 + 行为
举例使用:学生信息
class Student
{
//访问权限:公共权限
public:
//属性:学号和学号
string id;
string name;
//行为:
//1.输入学号和姓名
//2.输出姓名和学号
void Setid()
{
cin >> id;
}
void Setname()
{
cin >> name;
}
void Print()
{
cout << id << endl;
cout << name << endl;
}
};
int main()
{
//实例化对象,创造一个具体的学生
Student s;
cout << "请输入学生学号" << endl;
s.Setid();
cout << "请输入学生姓名" << endl;
s.Setname();
s.Print();
return 0;
}
8.2 封装意义2:
8.3 class和struct的区别
8.4 封装——设计案例
案例1:立方体
class Cube
{
//行为
public:
//设置长
void setL(int l)
{
L = l;
}
//获取长
int getL()
{
return L;
}
//设置宽
void setW(int w)
{
W = w;
}
//获取宽
int getW()
{
return W;
}
//设置高
void setH(int h)
{
H = h;
}
//获取高
int getH()
{
return H;
}
//计算体积
int caculateV()
{
return L * W * H;
}
//属性
private:
int L;//长
int W;//宽
int H;//高
};
int main()
{
Cube c;
c.setL(10);
c.setW(10);
c.setH(10);
cout << "c的体积为:" << c.caculateV() << endl;
return 0;
}
案例2:点和圆的关系
class Point
{
public:
void setX(int x)
{
X = x;
}
int getX()
{
return X;
}
void setY(int y)
{
Y = y;
}
int getY()
{
return Y;
}
private:
int X;
int Y;
};
class Circle//圆
{
public:
//设置半径
void setR(int r)
{
R = r;
}
//获取半径
int getR()
{
return R;
}
//设置圆心
void setCenter(Point center)
{
m_center = center;
}
//获取圆心
Point getCenter()
{
return m_center;
}
private:
int R;
Point m_center;
};
void IsInCircle(Point p, Circle c)
{
int distance = p.getX() * p.getX() + p.getY() * p.getY();
int r = c.getR();
if (r * r == distance)
{
cout << "点在圆上" << endl;
}
else if (r * r > distance)
{
cout << "点在圆内" << endl;
}
else
{
cout << "点在圆外" << endl;
}
}
int main()
{
Circle c;
Point center;
Point p;
center.setX(0);
center.setY(0);
c.setCenter(center);
c.setR(10);
p.setX(1);
p.setY(2);
IsInCircle(p, c);
return 0;
}
点和圆关系(分文件版)
主函数
#include<iostream>
using namespace std;
#include<string>
#include"源1.h"
int main()
{
Circle c;
Point center;
Point p;
center.setX(0);
center.setY(0);
c.setCenter(center);
c.setR(10);
p.setX(7);
p.setY(2);
IsInCircle(p, c);
return 0;
}
头文件
#pragma once
#include<iostream>
using namespace std;
#include<string>
class Point
{
public:
void setX(int x);
int getX();
void setY(int y);
int getY();
private:
int X;
int Y;
};
class Circle//圆
{
public:
//设置半径
void setR(int r);
//获取半径
int getR();
//设置圆心
void setCenter(Point center);
//获取圆心
Point getCenter();
private:
int R;
Point m_center;
};
void IsInCircle(Point p, Circle c);
函数实现
注意!类里面的函数,在实现时要声明是哪个类里的函数
#include<iostream>
using namespace std;
#include<string>
#include"源1.h"
void Point::setX(int x)
{
X = x;
}
int Point::getX()
{
return X;
}
void Point::setY(int y)
{
Y = y;
}
int Point::getY()
{
return Y;
}
//设置半径
void Circle::setR(int r)
{
R = r;
}
//获取半径
int Circle::getR()
{
return R;
}
//设置圆心
void Circle::setCenter(Point center)
{
m_center = center;
}
//获取圆心
Point Circle::getCenter()
{
return m_center;
}
void IsInCircle(Point p, Circle c)
{
int distance = p.getX() * p.getX() + p.getY() * p.getY();
int r = c.getR();
if (r * r == distance)
{
cout << "点在圆上" << endl;
}
else if (r * r > distance)
{
cout << "点在圆内" << endl;
}
else
{
cout << "点在圆外" << endl;
}
}
9.构造函数和析构函数的介绍与使用
9.1 构造函数和析构函数的功能
构造函数负责对象的初始化,析构函数负责对象的清理
如果不写的话,这两个函数编译器也会自动调用
9.2 函数语法
class Person
{
public:
//构造函数
//1.没有返回值
//2.函数名与类名相同
//构造函数可以有参数,可以函数重载
//4.只会调用一次
Person()
{
cout << "构造函数调用" << endl;
}
//析构函数
//1.没有返回值
//2.函数名与类名相同,在函数名前加~
//析构函数没有参数,不能函数重载
//4.只会调用一次
~Person()
{
cout << "析构函数调用" << endl;
}
private:
int age;
};
void test()
{
Person p1;
}
int main()
{
test();
return 0;
}
9.3 构造函数的分类
按参数分
class Person
{
public:
Person()
{
cout << "构造函数无参调用" << endl;
}
Person(int a)
{
cout << "构造函数有参调用" << endl;
}
~Person()
{
cout << "析构函数调用" << endl;
}
private:
int age;
};
void test()
{
Person p1;
Person p2(5);
}
int main()
{
test();
return 0;
}
按类型分
class Person
{
public:
Person()
{
cout << "构造函数无参调用" << endl;
}
Person(int a)
{
age = a;
cout << "构造函数有参调用" << endl;
}
Person(const Person& p)//将传入的人的所有属性都拷贝过来
{
age = p.age;
}
~Person()
{
cout << "析构函数调用" << endl;
}
int getA()
{
return age;
}
private:
int age;
};
void test()
{
Person p1;
Person p2(5);
Person p3(p2);
cout << "p2的年龄为:" << p2.getA() << endl;
cout << "p3的年龄为:" << p3.getA() << endl;
}
int main()
{
test();
return 0;
}
9.4 构造函数的调用方法
但括号发使用默认函数的时候不要加(),否则编译器会认为这是函数的声明
例如:Person p1; 不能写成 Person p1();
9.5 构造函数的调用时机
什么时候才会调用构造函数?
1.最基础的,拷贝一个已经创建完毕的对象,来初始化一个新对象
2.值传递的方式给函数参数传值
类的值传递(普通值传递不调用构造函数,因为构造函数是类特有的),临时拷贝了一份,实际上就是编译器自动调用了拷贝函数
普通值传递不调用构造函数
3.值方式返回局部对象
值返回的时候,其实也是用拷贝函数临时拷贝了一份然后返回
验证:地址不同,表明确实是拷贝了一份值,然后再返回,而非直接将地址返回
9.6 构造函数调用规则
1.创建一个类,c++编译器会自动给每个类添加三个函数
默认构造函数(空实现,无参),析构函数(空实现),拷贝函数(值拷贝)
2.如果我们写了有参的构造函数,编译器就不再提供默认构造函数,但依然提供拷贝构造函数
如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
9.7 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区申请空间,进行拷贝操作
错误案例:
为什么会发生重复释放的错误呢?
图中p3拷贝p2的操作为浅拷贝操作,因为利用编译器默认的浅拷贝构造函数,就是做浅拷贝操作,而浅拷贝操作相当于,把p2的地址等原原本本的拷贝给了p3,所以p2和p3指向同一块内存(如图1)。当释放时,根据栈的后进先出原则,p3先被释放,然后p2再被释放,同一块内存重复释,导致代码崩溃。
图1
解决方法:将p1和p2开辟空间在堆上(深拷贝),指向不同的空间(如图2)
//自己实现一个拷贝构造函数
Person(const Person& p)
{
cout << "拷贝构造函数的调用" << endl;
age = p.age;
//hight = p.hight;编译器默认实现的就是这行代码
//实现深拷贝操作
hight = new int(*p.hight);
}
图2
完整代码
#include<iostream>
using namespace std;
#include<string>
#include"源1.h"
class Person
{
public:
Person()
{
cout << "构造函数无参调用" << endl;
}
Person(int a,int h)
{
age = a;
hight = new int(h);
cout << "构造函数有参调用" << endl;
}
//自己实现一个拷贝构造函数
Person(const Person& p)
{
cout << "拷贝构造函数的调用" << endl;
age = p.age;
//hight = p.hight;编译器默认实现的就是这行代码
//实现深拷贝操作
hight = new int(*p.hight);
}
~Person()
{
if (hight != NULL)
{
delete hight;//此处会发生重复释放,报错
hight = NULL;
}
cout << "析构函数调用" << endl;
}
int getA()
{
return age;
}
int getH()
{
return *hight;
}
private:
int age;
int* hight;
};
void test()
{
Person p2(5,180);
Person p3(p2);
cout << "p2的身高为:" << p2.getH() << endl;
cout << "p3的身高为:" << p3.getH() << endl;
}
int main()
{
test();
return 0;
}
9.8 构造函数初始化列表简便方法
更灵活版
9.9 类对象作为类成员
和结构体一样,当成结构体理解就行了。
10.静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为静态成员变量和静态成员函数,以下是静态成员的特点
10.1 静态成员变量
1.所有对象共享一份数据
2.在编译阶段分配内存:从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
3.类内声明,类外初始化:静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。
class Person
{
public:
static int m_A;//类内声明
};
int Person::m_A = 100;//类外初始化
void test1()
{
Person p1;
cout << p1.m_A << endl;
Person p2;
p2.m_A = 200;
cout << p1.m_A << endl;
}
int main()
{
test1();
return 0;
}
为什么将p2的m_A赋值为200,再打印p1的m_A时,发现也被修改为200?
因为静态成员变量,不属于某个对象上(p1,p2就是对象),所有对象都共享一份数据,例如再创建Person p3,p4,p5,它们的m_A都是共享一份数据的。
所以既然m_A都共享一份数据,我们就可以通过类名访问m_A
void test1()
{
Person p1;
//1.通过对象访问
cout << p1.m_A << endl;
Person p2;
p2.m_A = 200;
//2.通过类名访问
cout << Person::m_A << endl;
}
10.2 静态成员函数
1.所有对象共享一个函数(和上面静态成员变量相似),见图1
2.静态成员函数只能访问静态成员变量,见图2
图1
class Person
{
public:
static void func()
{
cout << "static void func调用" << endl;
}
};
void test1()
{
Person p1;
//1.通过对象访问
p1.func();
//2.通过类名访问
Person::func();
}
int main()
{
test1();
return 0;
}
图2
class Person
{
public:
static void func()//静态成员函数
{
m_A = 10;
m_B = 10;//报错,因为m_B不是静态成员变量
}
static int m_A;//静态成员变量
int m_B;//非静态成员变量
};
11. C++对象模型和this指针
11.1 空类占用1个字节
11.2 成员变量和成员函数分开存储
成员变量和成员函数分开存储,只有非静态成员变量才属于类上
class Person
{
public:
int m_A;//占用对象空间
static int m_B;//不占用对象空间
//函数也不占用对象空间,所有函数共享一个函数实例
void func1()
{
;
}
static void func2()
{
;
}
};
11.3 this指针概念
通过11.2我们知道在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向 被调用的成员函数 所属的对象。说的简单点,例如创建了一个对象p1,Person p1,那么p1的this指针就指向p1的地址,而*this就是p1这个本体,可以认为 *this == p1。(示例见下方代码)
this指针是隐含每一个非静态成员函数内的一种指针(函数本身会有)
this指针不需要定义,直接使用即可,是编译器自带的
写一段代码验证上面关于this的解释
class Person
{
public:
Person& test2(Person &p)
{
if (this == &p)
{
cout << "this等于p的地址,*this就是p" << endl;
}
return *this;
}
};
void test1()
{
Person p;
p.test2(p).test2(p).test2(p);
}
int main()
{
test1();
return 0;
}
this指针的用途:
1.当形参和成员
变量同名时,可用this指针来区分
class Person
{
public:
Person(int age)
{
age = age;//显然,会无法分辨哪个是传进来的age,哪个是要被赋值的age
}
int age;
};
解决方法1:把属性名修改
class Person
{
public:
Person(int age)
{
m_age = age;
}
int m_age;
};
解决方法2:用this指针指向正在调用的成员函数的对象
class Person
{
public:
Person(int age)
{
this->age = age;
}
int age;
};
2.在类的非静态成员函敬中返回对象本身,可使用return *this
class Person
{
public:
Person& PersonAgeAdd(Person &p)
{
if (this == NULL)//为保证代码的健壮性,做空指针判断
{ //例如:当Person p = NULL,那么其this指向p的地址,也为NULL
exit(0);
}
this->age += p.age;
return *this;//返回p2的本体
}
int age;
};
void test()
{
Person p1;
p1.age = 10;
Person p2;
p2.age = 10;
cout << p2.age << endl;
p2.PersonAgeAdd(p1).PersonAgeAdd(p1).PersonAgeAdd(p1);
//函数每次返回了p2,然后再进行下一次函数调用
cout << p2.age << endl;
}
注意!此处的&不能少
因为如果没有&,就是值返回,系统会调用拷贝函数,故返回的地址是拷贝出来的p2的地址,地址与p2不同,调用函数失败
12. const修饰成员函数
12.1 常函数
1.成员函数后加const,我们称这个函数为常函数
2.常函数内不能修改成员属性
3.但成员属性加上mutable后(相当于把这个成员设为一个特殊的值),常函数依然可以修改其成员
12.2 常对象
常对象只能访问常函数
class Person
{
public:
void func1() const
{
;
}
void func2()
{
;
}
int m_A;
mutable int m_B;
};
void test()
{
const Person p;//在对象前加上const,变成常对象
p.func1();
p.func2();//报错,常对象不能访问常函数
//因为常函数可以访问不加mutable的成员变量
//但常对象的设定就是不能访问不加mutable的成员变量
}
12.3 this指针的本质
函数原型
const Person* const this;
this指针的本质是指针常量,其指向不能被修改
13. 友元函数
生活中你的家有客厅(public),有你的卧室(private)
客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去
但是呢,你也可以允许 好基友 进去。
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类可以访问另一个类中私有成员
友元的关键字为 friend
13.1 全局函数做友元函数
class Building
{
friend void GoodGay(Building &b);//要在类里先声明某个函数为友元函数
//这个函数就能访问该类的私有成员
public:
void InitBuilding()
{
SittingRoom = "客厅";
BedRoom = "卧室";
}
string SittingRoom;//客厅
private:
string BedRoom;//卧室
};
void GoodGay(Building &b)
{
cout << "好基友正在访问" << b.SittingRoom << endl;
cout << "好基友正在访问" << b.BedRoom << endl;
}
void test()
{
Building b;
b.InitBuilding();
GoodGay(b);
}
int main()
{
test();
return 0;
}
13.2 类做友元
成员函数可以类内直接实现(见13.1),
也可以类内定义,类外实现(见13.2),
两种方法均可,但在项目里用后者比较好,因为使类内较为整洁,保证类内代码的可读性。
GoodGay类的实现
class GoodGay
{
public:
GoodGay();//构造函数
void visit();//行为
Building* build;//创建一个建筑类,后续访问建筑类里的成员
};
GoodGay::GoodGay()//在类外,要加上 :: 来区分该成员函数属于哪个类
{
build = new Building;
//疑问1:为什么要特地用new在堆区上开辟一个空间,将build存放在栈区?
//回答:因为如果不开辟在堆区而是在栈区,访问一次就销毁,再想调用就找不到了。
//疑问2:为什么不直接在GoodGay类里 创建建筑类时直接new,要特地在构造函数里new?
//回答:可能是这样写比较简洁明了,后续释放空间也方便点,是个好的代码风格。
}
void GoodGay::visit()
{
cout << "好基友正在访问" << build->SittingRoom << endl;
cout << "好基友正在访问" << build->BedRoom << endl;
//因为BedRoom是Building类的私有成员,所以记得在Building类里声明GoodGay是友元类,才能进行访问
}
Building类的实现
Building::Building()
{
SittingRoom = "客厅";
BedRoom = "卧室";
}
class Building
{
friend class GoodGay;//在Building类里声明GoodGay是友元类
public:
Building();
string SittingRoom;//客厅
private:
string BedRoom;//卧室
};
完整代码,顺序要调整,否则会报错(有待考证为什么)
class Building;
class GoodGay
{
public:
GoodGay();//构造函数
void visit();//行为
Building* build;//创建一个建筑类,后续访问建筑类里的成员
};
class Building
{
friend class GoodGay;//在Building类里声明GoodGay是友元类
public:
Building();
string SittingRoom;//客厅
private:
string BedRoom;//卧室
};
GoodGay::GoodGay()//在类外,要加上 :: 来区分
{
build = new Building;
//疑问1:为什么要特地用new在堆区上开辟一个空间,将build存放在栈区?
//回答:因为如果不开辟在堆区而是在栈区,访问一次就销毁,再想调用就找不到了。
//疑问2:为什么不直接在GoodGay类里 创建建筑类时直接new,要特地在构造函数里new?
//回答:可能是这样写比较简洁明了,后续释放空间也方便点,是个好的代码风格。
}
void GoodGay::visit()
{
cout << "好基友正在访问" << build->SittingRoom << endl;
cout << "好基友正在访问" << build->BedRoom << endl;
//因为BedRoom是Building类的私有成员,所以记得在Building类里声明GoodGay是友元类,才能进行访问
}
Building::Building()
{
SittingRoom = "客厅";
BedRoom = "卧室";
}
void test()
{
Building b;
GoodGay g;
g.visit();
}
int main()
{
test();
return 0;
}
13.3 成员函数做友元
跟13.2只是稍有不同,把 friend 其他类,改为friend 其他类内的成员函数。
class Building;
class GoodGay
{
public:
GoodGay();
void visit();
Building* build;
};
class Building
{
friend void GoodGay::visit();//在Building类里声明GoodGay的visit函数是友元函数
public:
Building();
string SittingRoom;//客厅
private:
string BedRoom;//卧室
};
GoodGay::GoodGay()
{
build = new Building;
}
void GoodGay::visit()
{
cout << "好基友正在访问" << build->SittingRoom << endl;
cout << "好基友正在访问" << build->BedRoom << endl;
}
Building::Building()
{
SittingRoom = "客厅";
BedRoom = "卧室";
}
void test()
{
Building b;
GoodGay g;
g.visit();
}
int main()
{
test();
return 0;
}
14. 运算符重载
概念:
C++中的运算符重载是指通过重载运算符,实现自定义类型的运算符行为和语义。运算符重载可以是成员函数或非成员函数,使用operator关键字标识。
如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。
有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。
14.1 加号运算符重载
可见,直接用加号来使对象相加是不行的,因为c++中的默认加号不能操作对象相加,所以我们要将 ”+“ 重载,使它能用于对象相加。重载的方式有两种,成员函数重载和全局函数重载,二者都可以(但能用成员函数重载尽量用成员函数重载)。
1.通过成员函数重载实现
class Person
{
public:
int m_A;
int m_B;
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 main()
{
Person p1 = { 10,10 };
Person p2 = { 10,10 };
Person p3 = p1 + p2;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
return 0;
}
本质调用
Person p3 = p1.operator+(p2);
但由于用了operator关键字,可以简化成这样
Person p3 = p1 + p2;
通过全局函数重载实现
class Person
{
public:
int m_A;
int m_B;
};
Person operator+(Person& p1, Person& p2)
{
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
int main()
{
Person p1 = { 20,10 };
Person p2 = { 10,10 };
Person p3 = p1 + p2;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
return 0;
}
本质调用
Person p3 = operator+(p1, p2);
简化为
Person p3 = p1 + p2;
2.
全局函数重载
class Person
{
public:
int m_A;
int m_B;
};
Person operator+(Person& p1, int n)
{
Person temp;
temp.m_A = p1.m_A + n;
temp.m_B = p1.m_B + n;
return temp;
}
int main()
{
Person p1 = { 20,10 };
Person p2 = { 10,10 };
Person p3 = p1 + 10;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
return 0;
}
成员函数重载
class Person
{
public:
int m_A;
int m_B;
Person operator+(int n)
{
Person temp;
temp.m_A = this->m_A + n;
temp.m_B = this->m_B + n;
return temp;
}
};
int main()
{
Person p1 = { 20,10 };
Person p2 = { 10,10 };
Person p3 = p1 + 10;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
return 0;
}
个人理解:
全局函数重载
Person operator+(Person& p1, Person& p2)
传进来的左参数(p2) + 传进来的右参数(p2)
左参数传入的是 ”+“ 左边的操作数,右参数传入的是 ”+“ 右边的操作数,要一一对应
p3 = p1 + p2
成员函数重载
Person operator+ (Person& p)
可以理解为调用此函数时,加号左边的操作数加上加号右边的操作数
例如:p3 = p1 + p2
p1就是加号左边的操作数,p2就是加号右边的操作数)
14.2 左移运算符重载
成员函数实现不了左移运算符重载
函数的返回值要写 ostream& ,因为打印完了 p1 ,还得打印p2,
如果返回值写成void,打印完p1返回void,就相当于 void << p2,无法再打印
如果写成 ostream& ,打印完p1返回cout,就相当于 cout << p2,可以打印,体现了链式编程思想
class Person
{
public:
int m_A;
int m_B;
};
ostream& operator<<(ostream& cout, Person& p)
{
cout << p.m_A << endl;
cout << p.m_B << endl;
return cout;
}
int main()
{
Person p1 = { 20,10 };
Person p2 = { 10,30 };
cout << p1 << p2;
return 0;
}
那么将函数的参数传入位置调换,可行吗?
不行。左参数传入的是 << 左边的操作数,右参数传入的是 << 右边的操作数,要一一对应
14.3 前置++和后置++运算符重载
用是否有占位参数来区分前置++和后置++的重载函数。
有占位参数的那个是后置++重载函数。
为什么?
在C++中,后置++运算符已经被定义为一个单独的运算符,因此编译器可以区分前置++和后置++运算符。
对于重载后置++运算符的类成员函数,它们需要具有一个int类型的占位参数来区分前置和后置++运算符。当后置++运算符被执行时,编译器会自动传递一个值为0的int类型的占位参数给该函数,从而使得编译器能够确定这是后置++运算符的重载版本。
因此,后置++运算符用占位参数int,编译器就能够知道这是后置++的重载版本。
class MyInteger
{
public:
int m_Num;
MyInteger& operator++()//前置++重载
{
m_Num++;
return *this;
}
MyInteger& operator++(int)//后置++重载,要加上占位参数int,
{ //这样编译器就知道这是后置++
MyInteger temp = *this;//注意!this的类型是MyInteger*,*this的类型是MyInteger
++m_Num;
return temp;
}
};
ostream& operator<<(ostream& cout, MyInteger myint)
{
cout << myint.m_Num << endl;
return cout;
}
int main()
{
MyInteger myint;
myint.m_Num = 100;
cout << ++myint;
cout << myint++;
cout << myint;
return 0;
}
14.4 赋值运算符重载
还未重载赋值运算符时,直接将指针进行赋值操作,会出错
class Person
{
public:
Person(int age)
{
m_A = new int(age);
}
~Person()//申请的空间记得手动释放
{
if (m_A != NULL)
{
delete m_A;
m_A = NULL;
}
}
int* m_A;//用指针来举例,重载"="时要进行深拷贝
};
int main()
{
Person p1(18);
Person p2(20);
p2 = p1;
cout << *p1.m_A << endl << *p2.m_A << endl;
return 0;
}
原因:
解决方法:
实现代码:
class Person
{
public:
Person(int age)
{
m_A = new int(age);
}
~Person()//申请的空间记得手动释放
{
if (m_A != NULL)
{
delete m_A;
m_A = NULL;
}
}
void operator=(Person& p)
{
if (m_A != NULL)//如果被赋值的那个,还有属性在堆区,
{ //则先释放干净,再进行深拷贝(会重新申请一块空间)
delete m_A;
}
m_A = new int(*p.m_A);
}
int* m_A;//用指针来举例,重载"="时要进行深拷贝
};
int main()
{
Person p1(18);
Person p2(20);
p1 = p2;
cout << *p1.m_A << endl << *p2.m_A << endl;
return 0;
}
还有一些可以完善的地方,如果说 p1 = p2 = p3,那么上面代码就不行了,因为函数返回的是void。
可以修改为以下代码:
class Person
{
public:
Person(int age)
{
m_A = new int(age);
}
~Person()//申请的空间记得手动释放
{
if (m_A != NULL)
{
delete m_A;
m_A = NULL;
}
}
//记得加 &
Person& operator=(const Person& p)
{
if (m_A != NULL)//如果被赋值的那个,还有属性在堆区,
{ //则先释放干净,再进行深拷贝(会重新申请一块空间)
delete m_A;
}
m_A = new int(*p.m_A);
return *this;
}
int* m_A;//用指针来举例,重载"="时要进行深拷贝
};
int main()
{
Person p1(18);
Person p2(20);
Person p3(30);
p1 = p2 = p3;
cout << *p1.m_A << endl << *p2.m_A << endl << *p3.m_A << endl;
return 0;
}
为什么用到了const?
14.5 关系运算符重载
class Person
{
public:
Person(string name, int age)
{
m_Name = name;
m_Age = age;
}
bool operator==(Person p)
{
if (m_Name == p.m_Name && m_Age == p.m_Age)
{
return true;
}
return false;
}
bool operator!=(Person p)
{
if (m_Name != p.m_Name || m_Age != p.m_Age)
{
return true;
}
return false;
}
string m_Name;
int m_Age;
};
int main()
{
Person p1 = { "Tom",20 };
Person p2 = { "Tom",20 };
if (p1 == p2)
{
cout << "相同" << endl;
}
else
{
cout << "不相同" << endl;
}
if (p1 != p2)
{
cout << "不相同" << endl;
}
else
{
cout << "相同" << endl;
}
return 0;
}
14.6 重载函数调用运算符
运算符是 (),这个重载函数也叫伪函数或仿函数
所谓伪函数,是指通过在一个类中重载函数运算符operator()
,使该类的对象可以像函数一样被调用的一种技术。因此伪函数也称为仿函数(functor),它实际上是一个函数对象。
举例
例1.
class MyPrint
{
public:
void operator()(string str)
{
cout << str << endl;
}
};
int main()
{
MyPrint test;
test("我好帅");
return 0;
}
例2.
class MyPrint
{
public:
int operator()(int a,int b)
{
return a + b;
}
};
int main()
{
MyPrint test;
cout << test(3, 4) << endl;
return 0;
}
例3.
函数匿名对象,和上面不同的是,执行完就立即被销毁,后续不可访问
class MyPrint
{
public:
int operator()(int a,int b)
{
return a + b;
}
};
int main()
{
cout << MyPrint()(3, 4) << endl;
return 0;
}
15. 继承
继承的最大好处就是减少重复代码
15.1 基本语法
语法: class 子类 : 继承方式 父类
子类又称派生类,父类又称基类
普通实现:
可以看到每个类里有很多重复的代码,例如 header 和 left。
class Java
{
public:
void header()
{
cout << "首页 公开课 免费课 试看课 注册 登录" << endl;
}
void left()
{
cout << "帮助中心 交流合作 站内地图" << endl;
}
void content()
{
cout << "Java学习视频" << endl;
}
};
class CPP
{
public:
void header()
{
cout << "首页 公开课 免费课 试看课 注册 登录" << endl;
}
void left()
{
cout << "帮助中心 交流合作 站内地图" << endl;
}
void content()
{
cout << "C++学习视频" << endl;
}
};
class Python
{
public:
void header()
{
cout << "首页 公开课 免费课 试看课 注册 登录" << endl;
}
void left()
{
cout << "帮助中心 交流合作 站内地图" << endl;
}
void content()
{
cout << "Python学习视频" << endl;
}
};
void test()
{
Java java;
java.header();
java.left();
java.content();
cout << "--------------------" << endl;
CPP cpp;
cpp.header();
cpp.left();
cpp.content();
cout << "--------------------" << endl;
Python python;
python.header();
python.left();
python.content();
cout << "--------------------" << endl;
}
int main()
{
test();
return 0;
}
继承实现:
可以将类中相同重复的部分,写成一个父类
class Base
{
public:
void header()
{
cout << "首页 公开课 免费课 试看课 注册 登录" << endl;
}
void left()
{
cout << "帮助中心 交流合作 站内地图" << endl;
}
};
class Java:public Base
{
public:
void content()
{
cout << "Java学习视频" << endl;
}
};
class CPP:public Base
{
public:
void content()
{
cout << "C++学习视频" << endl;
}
};
class Python:public Base
{
public:
void content()
{
cout << "Python学习视频" << endl;
}
};
void test()
{
Java java;
java.header();
java.left();
java.content();
cout << "--------------------" << endl;
CPP cpp;
cpp.header();
cpp.left();
cpp.content();
cout << "--------------------" << endl;
Python python;
python.header();
python.left();
python.content();
cout << "--------------------" << endl;
}
int main()
{
test();
return 0;
}
15.2 继承方式
父类的私有,无论什么方式继承,都不能访问
C++中可使用三种继承方式:
1.public 继承方式:基类中的所有 public 成员在派生类中为 public 属性;基类中的所有 protected 成员在派生类中为 protected 属性;基类中的所有 private 成员在派生类中不能使用。
2.protected 继承方式:基类中的所有 public 成员在派生类中为 protected 属性;基类中的所有 protected 成员在派生类中为 protected 属性;基类中的所有 private 成员在派生类中不能使用。
3.private 继承方式:基类中的所有 public 成员在派生类中均为 private 属性;基类中的所有 protected 成员在派生类中均为 private 属性;基类中的所有 private 成员在派生类中不能使用。
15.3 继承中的对象模型
class Father
{
public:
int a;
protected:
int b;
private:
int c;
};
class Son:public Father
{
public:
int d;
};
void test()
{
cout << sizeof(Son) << endl;
}
int main()
{
test();
return 0;
}
结论:父类中所有非静态成员都会被继承,即使是父类中的私有成员不能访问,但也一样被继承了
15.5 继承中构造与析构的顺序
class Father
{
public:
int a;
Father()
{
cout << "Father构造函数调用" << endl;
}
~Father()
{
cout << "Father析构函数调用" << endl;
}
protected:
int b;
private:
int c;
};
class Son:public Father
{
public:
int d;
Son()
{
cout << "Son构造函数调用" << endl;
}
~Son()
{
cout << "Son析构函数调用" << endl;
}
};
void test()
{
Son s;
}
int main()
{
test();
return 0;
}
总结:先构造父类,再构造子类。先析构子类,再析构父类。
15.6 同名成员处理
子类跟父类都出现了成员变量a,那么访问的时候优先访问子类的a
class Father{
public:
int a;
Father(){
a = 200;
}
};
class Son:public Father{
public:
int a;
Son(){
a = 100;
}
};
void test(){
Son s;
cout << s.a << endl;
}
int main(){
test();
return 0;
}
那么该如何访问父类的a呢?
加上作用域就行了
class Father{
public:
int a;
Father(){
a = 200;
}
};
class Son:public Father{
public:
int a;
Son(){
a = 100;
}
};
void test(){
Son s;
cout << "子类:" << s.a << endl;
cout << "父类:" << s.Father::a << endl;
}
int main(){
test();
return 0;
}
成员函数也同理,如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏父类中所有同名成员函数(无论函数是否可以重载)
class Father{
public:
void func()
{
cout << "调用父类func()" << endl;
}
void func(int n)
{
cout << "调用父类func(int n)" << endl;
}
};
class Son:public Father{
public:
void func()
{
cout << "调用子类func()" << endl;
}
};
void test(){
Son s;
s.func();//结果是:调用子类func()
s.func(3);//不能通过函数重载的方式调用父类的同名成员函数
}
int main(){
test();
return 0;
}
同理,加上作用域就能解决
void test(){
Son s;
s.func();
s.Father::func();
s.Father::func(3);
}
所以,当成员名重复的时候,要判断好是否加作用域!
15.7 多继承语法
C++允许一个类继承多个类
语法: class 子类 : 继承方式 父亲1,继承方式 父亲2......
然而,多继承的多个父类可能有相同成员名,所以要加作用域区分
class Father1{
public:
int a;
Father1()
{
a = 100;
}
};
class Father2{
public:
int a;
Father2()
{
a = 200;
}
};
class Son :public Father1, public Father2 {
public:
};
int main() {
Son s;
cout << s.Father1::a << endl;
cout << s.Father2::a << endl;
return 0;
}
15.8 菱形继承
概念:先有两个派生类(子类)继承同一个基类(父类),然后又有某个类同时继承这两个派生类。这种继承被称为菱形继承,或钻石继承。如图
但如果两个父类都有相同的数据,然后同时继承了这两个父类,就相当于子类同时继承了两份相同的数据,导致资源浪费及毫无意义。例如图
解决办法:利用虚继承解决
当子类继承多个基类时,如果多个基类中都定义了同名的成员变量或函数,就会引发重名问题。为了解决这个问题,C++中提出了虚拟继承。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,在这种机制下,无论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员,从而避免了“菱形继承”问题带来的二义性和冲突。
在这个例子中,通过将类B和类C均以虚拟的方式继承自类A,使得D类只继承了一份类A的实例。这样就避免了在D类中出现两份相同数据(a)的情况。
class A {
public:
int a;
};
//类B为虚基类
class B : virtual public A {
public:
int b;
};
//类C为虚基类
class C : virtual public A {
public:
int c;
};
//无论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员
//所以类D不会重复包含类A的a
class D : public B, public C {
public:
int d;
};
16. 多态
16.1 多态的基本概念
下面通过案例来讲解多态
静态多态:C++ 中的静态多态是在编译期决定的,也称为早绑定(Early Binding),在程序编译时就可以确定函数的调用地址。
class Animal {
public:
void speak() {
cout << "动物说话了" << endl;
}
};
class Dog :public Animal {
public:
void speak() {
cout << "小狗说话了" << endl;
}
};
class Cat :public Animal {
public:
void speak() {
cout << "小猫说话了" << endl;
}
};
void doSpeak(Animal& animal) {
animal.speak();
}
int main() {
Cat cat;
Dog dog;
doSpeak(cat);
doSpeak(dog);
return 0;
}
我们理想的输出是,小猫说话了,小狗说话了,但为什么没有调用这两个函数,而是调用Animal里的函数呢?
因为以上代码是静态多态,
C++ 中的静态多态是在编译期决定的,也称为早绑定(Early Binding),在程序编译时就可以确定函数的调用地址。
在本代码中,Animal
的 speak()
函数和 Cat
、Dog
的 speak()
函数都不是虚函数,因此,在编译期就已经确定了调用的函数地址,不会再进行动态的函数查找。
因此,当 Cat
和 Dog
类型的对象通过 Animal
类型的引用或指针来调用 speak()
函数时,编译器只会调用 Animal
类中定义的 speak()
函数,并不会调用 Cat
或 Dog
自己的 speak()
函数。这就是静态多态的原理。
动态多态:在基类中将共同的功能声明为多个公共虚函数接口,子类通过重写(override)这些接口,实现自己的特殊行为。当程序运行时,通过基类指针或引用调用虚函数,会根据指针或引用所指向的实际对象类型来确定调用哪个版本的成员函数,即动态绑定或晚绑定。
如何在静态多态的基础上修改:很简单,只要在函数前加上个virtual使其变成虚函数就行。
代码分析:
在这段代码中,Animal
的 speak()
函数被声明为虚函数,在派生类中重写(override)该函数。因此,当通过基类指针或引用调用 speak()
函数时,会根据指针或引用所指向的对象类型来确定调用哪个版本的成员函数。这种情况下,就产生了动态多态(Dynamic Binding),也叫晚绑定(Late Binding)。
因此,当传入 Cat
或 Dog
的对象时,会分别调用它们自己的 speak()
函数,因为它们重写了基类 Animal
的 speak()
函数。具体来说,当传入 Cat
对象时,会调用 Cat
的 speak()
函数,打印出“小猫说话了”;当传入 Dog
对象时,会调用 Dog
的 speak()
函数,打印出“小狗说话了”。
class Animal {
public:
virtual void speak() {
cout << "动物说话了" << endl;
}
};
class Dog :public Animal {
public:
//子类重写父类虚函数
void speak() {
cout << "小狗说话了" << endl;
}
};
class Cat :public Animal {
public:
//子类重写父类虚函数
void speak() {
cout << "小猫说话了" << endl;
}
};
//父类指针或引用指向子类对象
void doSpeak(Animal& animal) {
animal.speak();
}
int main() {
Cat cat;
Dog dog;
doSpeak(cat);
doSpeak(dog);
return 0;
}
总结:
多态满足条件:
1.有继承关系。 2.子类重写父类中虚函数
多态使用:父类指针或引用指向子类对象
16.2 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,因为主要都是调用子类重写的内容。例如16.1案例中的虚函数Animal中的实现就没什么意义,案例中写出来只是为了便于理解晚绑定的概念
class Animal {
public:
virtual void speak() {
//毫无意义
cout << "动物说话了" << endl;
}
};
因此可以将虚函数改为纯虚函数
纯虚函数语法: virtual返回值类型 函数名 (参数列表)=0;
所以
virtual void speak() = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
。无法实例化对象
。子类必须重写抽象类中的纯虚函数,否则也属于抽象类
正确代码
16.3 虚析构和纯虚析构
为什么要有虚析构和纯虚析构?
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
这个案例可以看出,如果不使用虚析构或者纯虚析构,子类开辟到堆区的空间是无法释放的,造成内存泄漏。
class Animal {
public:
void speak() {
;
}
~Animal() {
cout << "Animal析构函数调用" << endl;
}
};
class Cat: public Animal{
public:
void speak() {
cout << "cat speak" << endl;
}
~Cat() {
cout << "cat析构函数调用" << endl;
}
};
int main()
{
Animal* animal = new Cat;
delete animal;
return 0;
}
而虚析构和纯虚析构的用法最主要的共同点是,都在父类的析构函数前加上virtual。
其他还有一些细微的差别,下面介绍。
虚析构使用
class Animal {
public:
void speak() {
;
}
//在父类的析构函数前加上virtual
virtual ~Animal() {
cout << "Animal析构函数调用" << endl;
}
};
class Cat: public Animal{
public:
void speak() {
cout << "cat speak" << endl;
}
~Cat() {
cout << "cat析构函数调用" << endl;
}
};
int main()
{
Animal* animal = new Cat;
delete animal;
return 0;
}
纯虚析构使用
与虚析构不同的是,纯虚析构需要类内声明,类外实现
class Animal {
public:
void speak() {
;
}
//类内声明
virtual ~Animal() = 0;
};
//类外实现
Animal::~Animal() {
cout << "Animal纯虚析构函数调用" << endl;
}
class Cat: public Animal{
public:
void speak() {
cout << "cat speak" << endl;
}
~Cat() {
cout << "cat析构函数调用" << endl;
}
};
int main()
{
Animal* animal = new Cat;
delete animal;
return 0;
}
16.4 多态使用的案例 --- 做饮品
class AbstractDrinking {
public:
virtual void Boil() = 0;//烧水
virtual void Brew() = 0;//冲泡
virtual void Pour() = 0;//倒入杯中
virtual void gulugulu() = 0;//咕噜咕噜,搁楞搁楞
//以上纯虚函数都会在子类里被重写
//向下面这样的函数就不用被重写
//因为继承本质上是为了减少代码的重复
//而这个函数是所有类都可以共同使用的函数
void makeDrink() {
Boil();
Brew();
Pour();
gulugulu();
}
};
class Coffee :public AbstractDrinking {
public:
void Boil() {
cout << "烧开水" << endl;
}
void Brew() {
cout << "泡咖啡" << endl;
}
void Pour() {
cout << "咖啡倒入杯中" << endl;
}
void gulugulu() {
cout << "搅拌咖啡" << endl;
}
};
class Tea :public AbstractDrinking {
public:
void Boil() {
cout << "烧开水" << endl;
}
void Brew() {
cout << "泡茶" << endl;
}
void Pour() {
cout << "茶倒入杯中" << endl;
}
void gulugulu() {
cout << "搅拌茶叶" << endl;
}
};
void doWork(AbstractDrinking* abs) {
abs->makeDrink();
delete abs;
}
void test() {
doWork(new Coffee);
cout << "--------------------" << endl;
doWork(new Tea);
}
int main() {
test();
return 0;
}