内存区
//程序运行前
-
代码区
特点:共享、只读
-
全局区
- 静态变量(static)
- 常量
- 字符串常量
- 全局常量(const修饰的全局变量)
//程序运行后
-
栈区
由编译器自动分配释放,存放函数的参数值(形参数据 ),局部变量等。栈区的数据在函数运行完后自动释放。
不要返回局部变量的地址!
-
堆区
由程序员分配释放,程序结束时由操作系统回收。
//利用new关键字将数据开辟在堆区 // new 数据类型 (初始值) 返回创建的数据的地址 int *p = new int(10); delete p;
new运算符
-
new的语法
//在堆区创建长度为10的整型数组 int *arr = new int[10];//[10]表示数组长度为10 for(int i = 0; i < 10; i++) arr[i] = i + 10;//给十个元素赋值 for(int i = 0; i < 10; i++) cout << a[i] << endl; //释放数组时要加[] delete[] arr;
-
引用
本质:指针常量,给变量起别名
int a = 10;
int &ref = a;
//自动替换成
int* const ref = &a;
函数的参数默认是值传递,即创造一个与实参数值相同的形参,如果调用函数时使用引用的方式,则相当于直接用实参进行运算(此时形参是实参的地址),这样更省空间。
引用语法:数据类型 & 别名 = 原名
int a = 10;
cout << a << endl;
int &b = a;//引用、初始化
b = 20;
cout << a << endl;
//运行结果
10
20
引用的注意事项:
- 引用必须初始化
- 引用初始化后不能改变
引用做返回值
-
不要返回局部变量的引用
原因与“不要返回局部变量”相同,因为局部变量会自动释放,可以用static修饰成为静态变量。
-
如果函数的返回值是引用,那么这个函数调用可以作为左值
常量引用
int &ref = 10;
//这种写法是错误的,引用本身需要一个合法的内存空间。
const int &ref = 10;
//加上const后正确,且之后不可修改变量,编译器自动优化代码为:int temp = 10; const int &ref = temp;
函数提高
函数默认参数
int func(int a, int b = 20, int c = 30)
{
return a + b + c;
}
int main()
{
cout << func(10) << endl;
//如果没有传入b、c,那么b、c按默认值运行,否则按传入的值运行
}
注意:
- 从第一个有默认参数的形参开始,之后的所有形参都必须有默认参数。
- 函数的声明(带分号时)和实现(写逻辑时)不能同时有参数。
函数占位参数
//返回值类型 函数名(数据类型){}
void func(int a, int){}
//第一个是形参,第二个是占位参数,在main函数中要有两个int型参数才能调用函数func
void func(int a, int = 10){}
//占位参数可以赋默认值,此时只需要一个int型参数就可以调用func
函数重载
作用:函数名可以相同,提高复用性
函数重载前提条件:
- 同一个作用域下
- 函数名称相同
- 函数参数 类型不同 or 个数不同 or 顺序不同
注意:
-
函数的返回值不同不可以作为函数重载的前提条件
-
引用作为函数重载的前提条件
//函数参数类型不同 void func(int &a){}//参数是变量 void func(const int &a){}//参数是常量
-
函数重载碰到默认参数
void func(int a, int b = 10){} void func(int a){} //当在main函数中调用func函数且只有一个参数时,程序具有二义性,这种函数重载是错误的
类和对象
C++面向对象的三大特性:封装、继承、多态
万事万物皆为对象,对象有其属性和行为
例如人可以作为对象,属性有姓名、年龄、身高……,行为有走路、跑步、跳跃……
车可以作为对象,属性有轮胎、方向盘、车灯……,行为有载人、开空调、放音乐……
具有相同性质的对象可以抽象成为类,人属于人类,车属于车类。
人的属性就是成员
人类 jht 身高 ——>类 对象 成员
封装
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为驾驭权限控制
设计一个圆类,求圆的周长
#include<bits/stdc++.h>
using namespace std;
const double PI = 3.14;
class Circle
{
//访问权限
public://公共权限
//属性
int r;//圆的半径
//行为
double calculate_ZC()//计算圆的周长
{
return 2 * PI * r;
}
};
int main()
{
//实例化(通过一个类,创建一个属于这个类的对象的过程)
Circle c1;//创建一个属于圆类的对象(创建一个圆)
c1.r = 10;//给圆对象的属性进行赋值
cout << "圆的周长是" << c1.calculate_ZC() << endl;
}
访问权限
**公共权限 public:**成员 类内可以访问,类外可以访问。
**保护权限 protected:**成员 类内可以访问,类外不可以访问,子类可以访问父类。
**私有权限 private:**成员 类内可以访问,类外不可以访问,子类不可以访问父类。属性尽量设置为私有
PS:struct默认权限是公共,class默认权限是私有。
对象的初始化与清理
析构函数与构造函数
程序在调用对象前后进行初始化(构造)和清理(析构)
构造函数:
语法:类名( ){ }
- 没有返回值,没有void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象的时候会自动调用构造函数且只调用一次,无需手动调用。
- 在public作用域下
析构函数:
语法:~类名( ){ }
- 没有返回值,没有void
- 函数名称与类名相同,名称前加~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构函数且只调用一次,无需手动调用。
- 在public作用域下
构造函数的分类与调用
分类
按参数分类:无参构造(默认构造)、有参构造。
按类型分类:普通构造、拷贝构造
class Person
{
public:
Person(const Person &p)//拷贝构造
{
age = p.age//复制传入的Person
}
//不是拷贝构造的都是普通构造
Person()//无参构造
{
age = 10//编译器默认生成的构造函数就是一个无参构造
}
Person(int a)//有参构造
{
age = a
}
int age;
};
调用
构造函数有以下三种调用方式
int main()
{
//括号法
Person p1;//调用默认构造
Person p2 (10);//调用有参构造
Person p3 (p2);//调用拷贝构造
//显示法
Person p1;//调用默认构造
Person p2 = Person (10);//有参构造
Person p3 = Person (p2);//拷贝构造
Person(10);//匿名对象,调用完构造函数后立刻执行析构函数,即生成后立刻销毁
//隐式转换法
Person p4 = 10;//相当于 Person p4 = Person (10)
Person p4 = p5;//拷贝构造
}
构造函数的调用规则
默认情况下,c++编译器至少给一个类提供三个函数
- 默认构造函数,无参,函数体为空
- 默认析构函数,无参,函数体为空
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++编译器不再提供默认无参构造函数,但会提供默认拷贝构造函数
- 如果用户定义拷贝构造函数,c++编译器不再提供其他构造函数
深拷贝与浅拷贝
如果有属性在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝(即值拷贝)带来的问题
初始化列表
在调用类时自动给属性赋值
//传统方法
class Person
{
public:
Person(int a, int b, int c)
{
m_A = a;
m_B = b;
m_C = c;
}
int m_A, m_B, m_C;
};
//初始化列表
class Person
{
public:
Person() : a(10), b(20), c(30)
{
}
int a, b, c;
};
对象模型与this指针
类对象作为类成员
//假设人(类)的成员之一是手机(类)
class phone
{
public:
phone(string pname)
{
name = pname;
}
string name;
};
class people
{
public:
people(string name, string pname) : m_name(name), m_phone(pname)
{}//初始化列表,隐式构造,m_phone(pname) = phone m_phone = pname;
string m_name;
phone m_phone;
};
int main()
{
people p1("huawei","aphone");
cout << p1.m_name << ' ' << p1.m_phone.name;
}
//当自身类成员还是一个类时,先构造成员类,后构造自身类
//析构顺序相反,先析构自身类,后析构
静态成员
静态成员就是在成员变量和成员函数前加上关键字static
静态成员分为:
- 静态成员变量
- 所有对象共用同一份数据
- 在编辑阶段分配内存(全局区)
- 必须在类内声明,在类外初始化
- 静态成员函数
- 所有对象共用同一个函数
- 静态成员函数只能访问静态成员变量
静态成员变量
//静态成员变量示例
class Person
{
public:
static int a;//类内声明静态成员变量
};
int Person :: a = 100;//类外初始化静态成员变量
int main()
{
Person m;
cout << m.a;//打印结果为 100
Person k;
k.a = 200;//所有对象共用同一份数据
cout << m.a;//打印结果为 200
}
//静态成员变量不适于某个特定的对象上,所有对象共享同一份数据
//因此静态成员变量有两种访问方式
// 1. 通过对象访问
Person P
cout << p.a << endl;
// 2. 通过类名访问
cout << Person :: a << endl;
//静态成员变量也有访问权限
class Person
{
public:
static int a;//公共权限下
private:
static int b;//私有权限下
};
int Person :: a = 100;//类外初始化
int Person :: b = 200;//类外初始化
int main()
{
Person m;
cout << m.a;//打印结果为 100
cout << m.b;//不可打印,b在私有权限下,只能在类内访问,不能在类外访问
}
静态成员函数
//静态成员函数示例
class Person
{
public:
static int a;//静态成员变量
int b;//非成员变量
static void func()
{
a = 100;//静态成员函数可以访问静态成员变量
b = 100;//报错!!静态成员函数 不能 访问非静态成员变量
cout << "调用静态函数";
}
};
int Person :: a = 0;
int main()
{
//通过对象调用
Person p;
p.func();
//通过类名调用
Person::func();
}
成员变量和成员函数分开储存
//一个空的对象占一个字节内存,占一个字节是为了区分不同的类,每个空对象占有独一无二的内存空间
class people
{
};
int main()
{
people p;
cout << sizeof(p) << '\n';//结果为1
}
//非静态成员变量,属于类的对象,占据类的内存
class people
{
int a;
};
int main()
{
people p;
cout << sizeof(p) << '\n';//结果为4(只有一个int)
}
//其余变量或函数(静态成员函数 与 非静态成员函数),不属于类的对象,不占据类的内存,是共用的
class people
{
int a;//非静态成员变量
static int b;//静态成员变量
void func1() {}//非静态成员函数
static void func2() {}//静态成员函数
};
int main()
{
people p;
cout << sizeof(p) << '\n';//结果为4,只有非静态成员变量占据类的内存
}
this指针
在C++中,成员变量和成员函数是分开存储的。
每一个非静态成员函数都只会存在一份,可能会有多个同类型的对象调用这一个非静态成员函数。
这个非静态成员函数是如何区分哪个对象在调用自己呢?
this指针指向被调用的成员函数所属的对象
this指针隐含在每一个非静态成员变量中,不需要定义,可以直接使用。
this指针作用:1. 解决名称冲突 2. 返回对象本身用*this
//解决名称冲突
class Person
{
public:
Person(int age)
{
age = age;//形参age和这两个age被当做一个变量
//this->age = age; 正确写法
//由于this指针指向被调用的成员函数所属的对象,即对象p
//代码即p->age = age;
}
int age;//名称冲突,这个int成员并没有被赋值
};
void main()
{
Person p(18);
cout << p.age;//输出错误
}
class Person
{
public:
Person(int x)
{
age = x;
}
Person& Add_age(Person &q)//返回引用,即p本身。如果没有&则是返回一个p的拷贝
{
this->age += q.age;
return *this;//返回p本身
}
int age;//名称冲突,这个int成员并没有被赋值
};
int main()
{
Person p(10), q(10);
p.Add_age(q).Add_age(q);//因为成员函数Add_age()返回p本身,所以每一对p.Add_age(q)都等效为年龄相加后的p。 链式编程
cout << p.age;//输出30 10 + 10 + 10
return 0;
}
空指针访问成员函数
class Person
{
public:
void Sayhello()
{
cou << "hello";
}
void Show_age()
{
cout << age;
//编译器实际理解为 cout << this->age
//但由于p是空指针,所以this为空指针,即NULL->age,报错。
}
int age = 10;
};
int main()
{
Person *p = NULL;
p.Sayhello();//正常运行
p.Show_age();//报错
}
const修饰成员函数
常函数
const修饰成员函数后,函数称为常函数。
常函数内不能修改成员属性。
成员属性声明时加关键字mutable后,便可以在常函数中修改。
class Person
{
public:
void Show_age() const
{
age = 10;//报错,因为函数是常函数,常函数内不能修改成员属性。
money = 10;//可以修改
}
int age;
mutable int money;
};
/*
原理:
age = 10 在编译器看来是 this->age = 10;
this指针本质上是一个指针常量,指针常量的指向是不能修改的。
Person * const this 即指针常量this的指向不能修改,但所指向的值是可以修改的。
const Person * const this 前面再加一个const使得指针常量所指向的值也不能修改。
因此,在成员函数后面加const实际上修饰的是this指向,使得this指针所指向的值也不能被修改
*/
常对象
声明对象前加const称该对象为常对象
常对象只能调用常函数
class Person
{
public:
void Show_age() const
{
age = 10;//报错,因为函数是常函数,常函数内不能修改成员属性。
money = 10;//可以修改
}
void func()
{
}
int age;
mutable int money;
};
int main()
{
const Person p;//在对象前加cosnt,变为常对象
p.Show_age();//可以调用
p.func();//不能执行
//常对象只能调用常函数。因为常对象是不能修改的,普通的成员函数有可能更改常对象的属性。
}
友元
友元的目的是让一个函数或者类访问另一个类中的私有成员
友元的三种实现
全局函数做友元
//建筑物类
class Building
{
friend void friend_func(Building &building)
//全局函数friend_func()是类Building的友元,可以访问Building类中的私有成员
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//全局函数
void friend_func(Building &building)
{
cout << building.m_SittingRoom << endl;
cout << building.m_BedRoom << endl;//声明友元后才能访问私有成员
}
int main()
{
Building building;
friend_func(&building);
}
类做友元
#include <iostream>
#include <string>
using namespace std;
class Building; // 提前声明 Building 类
class Friend_class
{
public:
Friend_class();
void visit(); // 参观函数,访问 Building 中的属性
Building *building;
};
class Building
{
friend class Friend_class;
public:
Building();
string m_SettingRoom;
private:
string m_BedRoom;
};
// 类外实现 Friend_class 的构造函数
Friend_class::Friend_class()
{
// 创建建筑物对象
building = new Building;
}
// 类外实现 Building 的构造函数
Building::Building()
{
m_SettingRoom = "客厅";
m_BedRoom = "卧室";
}
// 类外实现 Friend_class 的访问函数
void Friend_class::visit()
{
cout << building->m_SettingRoom << endl;
cout << building->m_BedRoom << endl;
}
int main()
{
Friend_class ff;
ff.visit();
return 0;
}
成员函数做友元
#include <iostream>
#include <string>
using namespace std;
class Building;
class Friend_class
{
public:
Friend_class();
void friend_visit();//成员函数做友元,可以访问Building中的私有成员
void visit();//普通成员函数不能访问privite成员
Building *building;
};
class Building
{
//Building类的实现要写在Friend_class后面,因为这里要使用Friend_class的成员函数friend_visit()
friend void Friend_class::friend_visit();//声明成员函数做友元
public:
Building();
string m_SettingRoom;
private:
string m_BedRoom;
};
//类外实现成员函数
Building::Building()
{
m_SettingRoom = "客厅";
m_BedRoom = "卧室";
}
Friend_class::Friend_class()
{
building = new Building;
}
void Friend_class::friend_visit()
{
cout << "友元访问" << building->m_SettingRoom << endl;
cout << "友元访问" << building->m_BedRoom << endl;//友元成员函数可以访问私有成员
}
void Friend_class::visit()
{
cout << "成员函数访问" << building->m_SettingRoom << endl;
//cout << "成员函数访问" << building->m_BedRoom << endl;
}
int main()
{
Friend_class ff;
ff.friend_visit();
ff.visit();
}
运算符重载
运算符重载概念:对已有的运算符重新进行定义以适应不同的数据类型。
加号运算符重载
重定义加号为将两个Person类的成员m_a、m_b对应相加。
#include <iostream>
#include <string>
using namespace std;
//重载加号运算符
class Person
{
public:
int m_a, m_b;
//通过成员函数重载加号
Person operator + (Person &p)
{
Person temp;;
temp.m_a = this->m_a + p.m_a;//这里this->不是必须的
temp.m_b = this->m_b + p.m_b;
return temp;
}
};
/*
通过全局函数重载加号
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;
}
int main()
{
Person p1;
p1.m_a = 10;
p1.m_b = 20;
Person p2;
p2.m_a = 1;
p2.m_b = 2;
Person p3;
p3 = p1 + p2;
//重载加号的本质
//p3 = p1.operator+(p2); 成员函数
//p3 = operator+(p1, p2); 全局函数
cout << p3.m_a << ' ' << p3.m_b;
p3 = p3 + 10;//运算符重载 的 函数重载
cout << p3.m_a << ' ' << p3.m_b;
}
左移运算符重载
重载左移运算符可以实现输出自定义数据类型。
class Person
{
public:
int m_a, m_b;
};
//重构左移运算符一般用全局函数
ostream & operator << (ostream &cout, Person &p)
//cout的数据类型是输出流ostream
//参数写 ostream &out 也可以,因为引用的本质是起别名,ostream &out = cout
{
cout << "m_a = " << p.m_a << " m_b = " << p.m_b;
return cout;//链式构造法则
}
int main()
{
int a = 10;
cout << a << endl; //这样可以直接输出10
Person p;
p.m_a = 10;
p.m_b = 20;
cout << p << endl; //重构左移运算符后,可以直接输出p的两个成员变量
}
自增运算符重载
//先利用友元和重载左移运算符的知识写一个自定义的整型数据类型
#include <iostream>
#include <string>
using namespace std;
class Myint
{
friend ostream& operator << (ostream& cout, Myint my);
public:
Myint()
{
m_int = 0;
}
private:
int m_int;
};
ostream& operator << (ostream& cout, Myint my)
{
cout << my.m_int << endl;
return cout;
}
int main()
{
Myint my;
cout << my;
}
如何重载自增运算符使得自定义的整形数据类型也能实现自增?
#include <iostream>
#include <string>
using namespace std;
class Myint
{
friend ostream& operator << (ostream& cout, Myint my);
public:
Myint()
{
m_int = 0;
}
//重载前置自增运算符
Myint& operator ++ ()//注意,这里返回Myint类型的引用,这是为了一直对同一个数据进行递增操作
{
//先递增
m_int++;
//后返回
return *this;//返回调用这个成员函数的对象本身
}
//重载后置自增运算符
//int是占位参数,用于区分前置和后置递增,有int代表是后置递增。
Myint operator++(int)//注意,后置递增返回的是拷贝值,不是引用
{
//先记录
Myint temp = *this;
//再递增
m_int++;
//后返回记录值
return temp;
}
private:
int m_int;
};
ostream& operator << (ostream& cout, Myint my)
{
cout << my.m_int;
return cout;
}
int main()
{
Myint my;
cout << my << endl;
cout << my++ << endl;//这里cout要输出一个Myint类型的数据,因此自增运算符重载后的返回值应该是Myint类型
cout << my << endl;
cout << ++my << endl;
cout << my << endl;
}
前置递增返回的是引用,后置递增返回的是值。
赋值运算符重载
C++编译器至少给一个类默认增加四个函数
- 默认构造函数(无参、函数体为空)
- 默认析构函数(无参、函数体为空)
- 默认拷贝构造函数、对属性进行值拷贝
- 赋值运算符operator = ,对属性进行值拷贝
如果类中有属性指向堆区,那么做赋值运算时要考虑深浅拷贝问题,否则报错。
class Person
{
public:
Person(int age)//有参构造
{
m_age = new int(age);
//堆区开辟的空间需要程序员手动清除
}
~Person()
{
if(m_age != NULL)
{
delete m_age;
m_age = NULL;
}
}
int *m_age;
};
int main()
{
Person p1(18);
Person p2(20);
Person p3(33);
cout << "p1的年龄是:" << *p1.m_age << endl;
cout << "p2的年龄是:" << *p2.m_age << endl;
cout << "p3的年龄是:" << *p3.m_age << endl;
p3 = p2 = p1;//赋值操作,注意这里是浅拷贝,p1、p2都指向堆区同一片地址,导致这片地址被析构释放两次,报错。
cout << "p1的年龄是:" << *p1.m_age << endl;
cout << "p2的年龄是:" << *p2.m_age << endl;
cout << "p3的年龄是:" << *p3.m_age << endl;
}
自己写一个赋值运算,改为深拷贝解决问题
Person& operator = (Person &p)
{
//编译器提供的是类似的浅拷贝
//m_age = p.m_age
//正确写法:先判断是否有属性在堆区,有则先释放,然后再深拷贝。先判断的原因是在赋值之前,p2可能已经有值了。
if(m_age != NULL)
{
delete m_age;
m_age = NULL;
}
//深拷贝
m_age = new int(*p.m_age);
return *this;//返回自身是为了满足链式编程,函数头返回值应当为引用,这样才能返回自身,否则将返回一个值拷贝。
}
关系运算符重载
class Person
{
public:
Person(string name, int age)//有参构造
{
this->name = name;
this->age = age;
}
bool operator== (Person &p)
{
if(this->name == p.name && this->age == p.age)
{
return true;
}
return false;
}
bool operator!= (Person &p)
{
if(this->name == p.name && this->age == p.age)
{
return false;
}
return true;
}
string name;
int age;
};
int main()
{
Person p1("tom", 18);
Person p2("tom", 18);
if(p1 == p2)
{
cout << "相等" << endl;
}
else
{
cout << "不相等" << endl;
}
if(p1 != p2)
{
cout << "不相等" << endl;
}
else
{
cout << "相等" << endl;
}
}
函数调用运算符重载
- 函数调用运算符 () 也可以发生重载
- 由于重载后的使用方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定的写法,很灵活
#include<bits/stdc++.h>
using namespace std;
class Myprint
{
public:
void operator() (string text)
{
cout << text << endl;
}
};
class Myadd
{
public:
int operator() (int num1, int num2)
{
return num1 + num2;
}
};
int main()
{
Myprint test;
test("hello world");
Myadd add;
int ans = add(100, 20);
cout << ans << endl;
//匿名函数对象
cout << Myadd() (100, 20) << endl;
//匿名函数对象没有名称,使用后立即被释放
}
继承
下级别成员具有上一级的共性,也有自己的特性。
可以用继承的技术,减少重复代码。
继承的基本语法
例子:一个页面中大部分内容都是公共的(页头、页尾、左侧导航栏等),只有中间的内容部分是独有的。如果不使用继承,那么每写一个新页面都需要复制粘贴公共部分的代码,非常麻烦。
#include<bits/stdc++.h>
using namespace std;
//继承实现页面
class BasePage
{
public:
void header()
{
cout << "首页、公开课、登录、注册……(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图……(公共尾部)" << endl;
}
void left()
{
cout << "Java、Python、C++……(公共分类列表)" << endl;
}
};
//Java页面
class Java : public BasePage//继承语法
{
public:
void content()
{
cout << "Java学科视频" << endl;
}
};
//Python页面
class Python : public BasePage
{
public:
void content()
{
cout << "Python学科视频" << endl;
}
};
//CPP页面
class CPP : public BasePage
{
public:
void content()
{
cout << "CPP学科视频" << endl;
}
};
int main()
{
cout << "Java视频页面如下: " << endl;
Java java;
java.header();
java.footer();
java.left();
java.content();
cout << "……………………………………………………" << endl;
cout << "Python视频页面如下: " << endl;
Python python;
python.header();
python.footer();
python.left();
python.content();
cout << "……………………………………………………" << endl;
cout << "CPP视频页面如下: " << endl;
CPP cpp;
cpp.header();
cpp.footer();
cpp.left();
cpp.content();
cout << "……………………………………………………" << endl;
}
继承的好处:减少重复代码
语法: class 子类(派生类) : 继承方式 父类(基类)
子类也称作派生类,父类也称作基类
继承方式
三种继承方式:
- 公共继承
- 保护继承
- 私有继承
-
子类通过三种继承方式都不能访问到父类的私有内容
-
子类通过公共继承到的父类属性,权限不变
-
子类通过保护继承到的父类属性,权限全部为protected
-
子类通过私有继承到的父类属性,权限全部为private
继承中的对象模型
class base
{
public:
int m_a;
protected:
int m_b;
private:
int m_c;
};
class son : public base
{
public:
int m_d;
}
int main()
{
cout << "size of son =" << sizeof(son) << endl
}
//输出结果为16,父类所有属性都被子类继承
父类所有非静态成员属性都被子类继承。
父类中私有成员属性是被编译器隐藏了,因此访问不到。
继承中构造和析构顺序
在子类继承父类的过程中,构造和析构的顺序如下:
先构造父类,再构造子类,然后析构子类,最后析构父类。
继承同名成员的处理方式
当子类和父类出现同名的成员,如何通过子类对象,分别访问到子类和父类中同名的属性呢?
class base
{
public:
int m_a;
base()
{
m_a = 100;
}
void func()
{
cout << "父类成员函数调用" << endl;
}
};
class son : public base
{
public:
int m_a;
son()
{
m_a = 200;
}
void func()
{
cout << "子类成员函数调用" << endl;
}
};
int main()
{
son s;
cout << "子类m_a = " << s.m_a;//访问子类属性,输出200
//cout << "父类m_a = " << s.base::m_a;//访问父类属性,输出100
s.func();
s.base::func();
}
直接写“.”访问子类成员,加作用域后访问父类成员。
继承同名静态成员的处理方式
#include<bits/stdc++.h>
using namespace std;
class base
{
public:
static int m_a;
};
int base :: m_a = 100;
class son : public base
{
public:
static int m_a;
};
int son :: m_a = 200;
int main()
{
son s;
cout << "通过对象访问同名静态属性" << endl;
cout << "子类静态成员m_a = " << s.m_a << endl;
cout << "父类静态成员m_a = " << s.base::m_a << endl << endl;
cout << "通过类名访问同名静态属性" << endl;
cout << "子类静态成员m_a = " << son::m_a << endl;
cout << "子类静态成员m_a = " << son::base::m_a << endl;//第一个::表示通过类名方式访问,第二个::表示访问父类作用域下
}
多继承语法
C++允许一个类继承多个类。
语法: class 子类 : 继承方式 父类1, 继承方式 父类2…
父类中出现重名成员时需要加作用域区分。
菱形继承
概念:
两个派生类继承同一个基类
又有某个类同时继承这两个派生类
这种继承被称为菱形继承,或钻石继承
菱形继承的问题:
- 二义性
- 同一个成员属性被继承多次,占用空间。
class animal
{
public:
int m_age;
};
class sheep : public animal
{
};
class tuo : public animal
{
};
class sheeptuo : public sheep, public tuo
{
};
int main()
{
sheeptuo s;
s.tuo::m_age = 18;
s.sheep::m_age = 28;
//菱形继承,两个父类拥有同名数据,需要加作用域以区分
cout << "s.tuo::m_age =" << s.tuo::m_age << endl;
cout << "s.sheep::m_age =" << s.sheep::m_age << endl;
//我们知道m_age这个数据有一份即可,但是菱形继承导致有两份数据,浪费资源。
}
利用虚继承来解决菱形继承的问题(虚继承是一种继承方式)
继承之前加上关键字virtual变成虚继承
animal类称为虚基类
class animal
{
public:
int m_age;
};
class sheep : virtual public animal
{
};
class tuo : virtual public animal
{
};
class sheeptuo : public sheep, public tuo
{
};
int main()
{
sheeptuo s;
s.tuo::m_age = 18;
s.sheep::m_age = 28;
//虚继承不需要加作用域区分,数据只有一份。
cout << "s.tuo::m_age =" << s.tuo::m_age << endl;
cout << "s.sheep::m_age =" << s.sheep::m_age << endl;
cout << "s.m_age =" << s.m_age << endl;//虚继承可以直接用.索引
}
虚继承并不是继承数据,而是继承指针,通过指针找到唯一的数据
多态
多态语法
- 静态多态:函数重载 和 运算符重载 属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include<bits/stdc++.h>
using namespace std;
class Animal
{
public:
//virtual void speak() //虚函数
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()//子类重写父类的虚函数
{
cout << "小猫在说话" << endl;
}
};
//执行说话的函数
//这里是地址早绑定 - 在编译阶段确定函数地址,即无论输入的是Animal的哪个子类,调用的都是Animal的成员属性。
//如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,即地址晚绑定
void doSpeak(Animal &animal)//Animal &animal = cat 父类的引用 指向子类对象
{
animal.speak();
}
int main()
{
Cat cat;
doSpeak(cat);
//C++允许父子之间的强制类型转换
}
动态多态的满足条件:
- 有继承关系
- 子类重写父类的虚函数(父类虚函数必须写virtual,子类重写的函数前可写可不写virtual)
重写概念: 函数返回值类型、函数名、参数列表完全相同
动态多态的使用:
父类的指针或者引用 指向子类对象
多态原理
class Animal
{
public:
virtual void speak() //虚函数
{
cout << "动物在说话" << endl;
}
};
//此时Animal类内是一个vfptr(虚函数指针virtual function pionter),它指向一个vftable(虚函数表)。这个vftable内部记录虚函数地址&Animal::speak()
class Cat : public Animal
{
public:
};
//此时子类仅继承了父类,并没有重写。Cat类内与上述的Animal类内结构相同。即Cat类内是一个vfptr,它指向一个vftable。这个vftable内部记录虚函数地址&Animal::speak()
class Cat : public Animal
{
public:
void speak()//子类重写父类的虚函数
{
cout << "小猫在说话" << endl;
}
};
//当子类重写父类的虚函数,子类中的虚函数表 内部 会替换成 子类的虚函数地址。即vftable内部记录的虚函数地址&Animal::speak() 被替换为 &Cat::speak()
Animal & animal = cat;
animal.speak();
//当父类的指针或者引用指向子类对象时,发生多态。从子类的虚函数表中找到对应的虚函数地址
多态案例1 - 计算器
分别利用普通写法和多态技术实现计算器
//普通写法
class Calculator
{
public:
int getResult(char oper)
{
if(oper == '+')
{
return num1 + num2;
}
else if(oper == '-')
{
return num1 - num2;
}
else if(oper == '*')
{
return num1 * num2;
}
else if(oper == '/')
{
return num1 / num2;
}
//如果想要增加新的运算,就需要修改这里的源码,这是不好的。
//在真实的开发中,提倡开闭原则,即对扩展进行开放,对修改进行关闭。
}
int num1, num2;
};
int main()
{
Calculator c;
c.num1 = 10;
c.num2 = 30;
cout << c.num1 << '+' << c.num2 << '=' << c.getResult('+') << endl;
cout << c.num1 << '-' << c.num2 << '=' << c.getResult('-') << endl;
cout << c.num1 << '*' << c.num2 << '=' << c.getResult('*') << endl;
cout << c.num1 << '/' << c.num2 << '=' << c.getResult('/') << endl;
}
//利用多态实现计算器
//实现计算器抽象类
class AbstractCalculator
{
public:
virtual int getResult()
{
return 0;
}
int num1;
int num2;
};
//加法计算器类
class AddCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1 + num2;
}
};
//减法计算器类
class SubCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1 - num2;
}
};
//乘法计算器类
class MulCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1 * num2;
}
};
int main()
{
//多态使用条件:父类指针或者引用指向子类对象
//加法运算
AbstractCalculator *abc = new AddCalculator;
abc->num1 = 100;
abc->num2 = 10;
cout << abc->num1 << '+' << abc->num2 << '=' << abc->getResult() << endl;
//new出来的东西用完后记得delete释放
delete abc;
//这一句是将堆区的数据释放了,但指针的类型没有变,还是父类的指针。
abc = new MulCalculator;
abc->num1 = 100;
abc->num2 = 10;
cout << abc->num1 << '*' << abc->num2 << '=' << abc->getResult() << endl;
//new出来的东西用完后记得delete释放
delete abc;
}
多态优点:
- 组织结构清晰,哪部分功能有错直接看对应代码即可。
- 可读性强
- 便于维护和扩展
纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是没有意义的,因此可以将虚函数改为纯虚函数
纯虚函数语法: virtual 返回值类型 函数名(参数列表)= 0;
当类中有纯虚函数,这个类也被称为抽象类
抽象类的特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类,也无法实例化对象
#include<bits/stdc++.h>
using namespace std;
class Base
{
public:
virtual void func() = 0;
};
class Son : public Base
{
public:
virtual void func()
{
cout << "func函数调用" << endl;
}
};
int main()
{
//错误写法,纯虚函数无法实例化对象
//Base b;
//new Base;
//通过实例化对象调用
//Son s;
//s.func();
//通过多态调用
Base *base = new Son;
base->func();
delete base;
}
多态案例2 - 制作饮品
#include<bits/stdc++.h>
using namespace std;
class AbstractDrinking
{
public:
virtual void Boil() = 0;//煮水
virtual void Brew() = 0;//冲泡
virtual void PourInCup() = 0;//倒入杯中
virtual void PutSomething() = 0;//加入辅料
void makeDrink()
{
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking
{
virtual void Boil()
{
cout << "煮农夫山泉" << endl;
}
virtual void Brew()
{
cout << "冲泡咖啡" << endl;
}
virtual void PourInCup()
{
cout << "倒入杯中" << endl;
}
virtual void PutSomething()
{
cout << "加入糖和牛奶" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking
{
virtual void Boil()
{
cout << "煮矿泉水" << endl;
}
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
virtual void PourInCup()
{
cout << "倒入杯中" << endl;
}
virtual void PutSomething()
{
cout << "加入枸杞" << endl;
}
};
//制作函数
void doWork(AbstractDrinking * abs)//AbstractDrinking * abs = new Coffee
{
abs->makeDrink();
delete abs;//释放堆区数据
}
int main()
{
//制作咖啡
doWork(new Coffee);
cout << "……………………" << endl;
doWork(new Tea);
}
虚析构和纯虚析构
使用多态时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构的共性:
- 可以解决父类指针不能释放子类对象的问题
- 都需要有具体的函数实现
区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
父类指针在析构的时候,不会调用子类中的析构函数而是直接调用父类的析构函数。此时如果子类中有堆区属性,那么会出现内存泄漏。
利用虚析构可以解决 父类指针释放子类对象时 释放不干净 的问题
解决方法:将父类的析构函数写为虚析构,即在前面加virtual关键字
例子:
#include<bits/stdc++.h>
using namespace std;
class Animal
{
public:
virtual void speak() = 0;
Animal()
{
cout << "Animal构造函数调用" << endl;
}
//virtual ~Animal() = 0;//改为纯虚析构,有了纯虚析构后,这个类属于抽象类,无法实例化对象
//如果在父类中有开辟在堆区的数据,那么也需要析构函数进行释放,因此写出析构函数的具体实现是必要的,只写“……=0”是错误的,需要在类外写出具体实现。
//virtual ~Animal() //改为虚析构
~Animal()
{
cout << "Animal析构函数调用" << endl;
}
};
//如果父类写纯虚析构函数,那么需要在类外写出实现
// Animal :: ~Animal()
// {
// cout << "Animal纯虚构造函数调用" << endl;
// }
class Cat : public Animal
{
public:
virtual void speak()
{
cout << "小猫" << *m_name << "在说话" << endl;
}
Cat(string name)
{
cout << "Cat构造函数调用" << endl;
m_name = new string(name);//将成员属性字符串创建在堆区并用指针维护
}
~Cat()
{
if(m_name != NULL)
{
cout << "Cat析构函数调用" << endl;
delete m_name;
m_name = NULL;
}
}
string *m_name;
};
int main()
{
Animal *animal = new Cat("Tom");
animal->speak();
delete animal;
}
总结:
- 虚析构和纯虚析构是用来解决通过父类指针无法释放子类对象在堆区中数据的问题
- 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
- 纯虚析构需要声明也需要实现