C++面向对象的三大特性:封装,继承,多态
意义:将属性和行为作为一个整体,表现生活中的事务,将属性和行为加以权限控制
语法:class 类名{ 访问权限: 属性/行为 };
const double PI = 3.14; //圆周率
class Circle {
public:
//属性
int m_r; //半径
//行为(用函数)
double calZC() { //计算周长
return 2 * PI * m_r;
}
};
int main()
{
//通过圆类,创建具体的圆(对象)
Circle c1;
//给圆对象的属性进行赋值
c1.m_r = 10;
cout << "圆c1的周长" << c1.calZC() << endl;
}
实例化:通过一个类创建一个对象的过程
class Student {
public:
//类中的属性和行为,统称为成员
//属性 = 成员属性 = 成员变量 行为 = 成员函数 = 成员方法
string m_Name;
int m_ID;
void setName(string name) { //给姓名赋值
m_Name = name;
}
void setID(int ID) {
m_ID = ID;
}
void showStudent() { //打印学生信息
cout << "姓名:" << m_Name << " 学号:" << m_ID << endl;
}
};
int main()
{
Student s1;
s1.setName("费沁源");
s1.setID(1);
s1.showStudent();
}
访问权限:
1.公共权限 public
成员在类内可以访问,类外也可以访问
待补充(继承)
2.保护权限 protected
成员在类内可以访问,类外不可以访问
子类可以访问父类的保护内容
3.私有权限 private
成员在类内可以访问,类外不可以访问
子类不可以访问父类的私有内容
struct默认权限为共有; class默认权限为私有
(struct也可以包含成员函数,可以继承,可以实现多态)
class可用于定义模板参数,struct不可以
优点:
1.可以自己控制读写权限
2.可以检测数据的有效性
class Person {
private:
string m_Name; //姓名 可读可写
char m_Sex; //性别 可读
int m_Age; //年龄 可读可写(对写入进行限制)
string m_Lover; //伴侣 可写
public:
void setName(string name) { //设置姓名
m_Name = name;
}
string getName() { //获取姓名
return m_Name;
}
int getAge() { //获取年龄
m_Age = -1; //初始化为-1
return m_Age;
}
void setAge(int age) { //设置年龄,可检验数值的有效性
if (age < 0 || age > 150) {
cout << "数据无效" << endl;
return;
}
m_Age = age;
}
void setLover(string lover) { //设置伴侣
m_Lover = lover;
}
};
C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
语法:类名(){ }
1.构造函数没有返回值,也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时会自动调用构造,无须手动调用,而且只会调用一次
语法:~类名(){ }
1.构造函数没有返回值,也不写void
2.函数名称与类名相同,在名称前加上~
3.构造函数不可以有参数,因此不可以发生重载
4.程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
两种分类方式:
按参数分:有参构造 和 无参构造(默认构造)
按类型分:普通构造 和 拷贝构造
//有参构造
Person() {
cout << "Person 无参构造函数调用" << endl;
}
//无参构造(默认构造)
Person(int a) {
age = a;
cout << "Person 有参构造函数调用" << endl;
}
//拷贝构造函数
Person(const Person &p) { //复制一个一模一样的,不能改变本体,所以使用const
age = p.age;
cout << "Person 拷贝构造函数调用" << endl;
}
三种调用方式:
括号法 显示法 隐式转换法
//括号法
//调用默认构造函数时不要加“()“
//编译器会将 Person p1(); 当作函数声明,不会认为在创建对象
Person p1; //默认构造函数调用
Person p2(10); //有参构造函数调用
Person p3(p2); //拷贝构造函数调用
//显示法
Person p1; //默认构造函数调用
Person p2 = Person(10); //有参构造函数调用
Person p3 = Person(p2); //拷贝构造函数调用
Person(10); //匿名对象,47行代码中,p2为它的名字
//特点:当前行执行结束后,系统会立即回收匿名对象
//不要利用拷贝构造函数初始化匿名对象
Person(p3); //错误
//编译器会认为Person(p3) == Person p3;看作一个对象的声明,
//隐式转换法
Person p4 = 10; //相当于写了 Person p4 = Person(10); 有参构造
Person p5 = p4; //拷贝构造
1.使用一个已经创建完毕的对象来初始化一个新对象
void test1()
{
Person p1(20);
Person p2(p1);
cout << "p2的年龄" << endl;
}
2.值传递的方式给函数参数传值
void func(Person p) //值传递的本质:会拷贝出一个临时的副本
{ //等于Person p = p; 拷贝构造函数的隐式写法
}
void test2()
{
Person p;
func(p);
}
Person func2()
{
Person p3;
cout << (int*)&p3 << endl;
return p3;
}
void test3()
{
Person p = func2();
cout << (int*)&p << endl;
}
默认情况下,C++编译器至少给一个类添加3个函数
构造函数调用规则:
class Person {
public:
Person() {
cout << "Person 默认构造函数调用" << endl;
}
Person(int age, int height) {
m_Age = age;
m_Height = new int(height); //用指针接收堆区的数据
cout << "Person 有参构造函数调用" << endl;
}
~Person() {
//析构函数的作用:将堆区开辟的数据做释放操作
if (m_Height != NULL) {
delete m_Height;
m_Height = NULL; //防止野指针,进行置空
}
cout << "Person 析构函数的调用" << endl;
}
int m_Age;
int *m_Height; //身高
};
void test1()
{
Person p1(18, 160);
cout << "p1的年龄为:" << p1.m_Age << "p1的身高为:" << *p1.m_Height << endl;
Person p2(p1);
cout << "p2的年龄为:" << p1.m_Age << "p2的身高为:" << *p1.m_Height << endl;
}
浅拷贝带来的问题:堆区内存的重复释放
浅拷贝是逐个字节完全复制,所以p2.m_Height的地址和p1.m_Height一样,当p2被释放时,执行析构函数,对p2.m_Height进行释放,当p1被释放时,执行析构函数,会对0x0011再次进行释放,从而导致错误
深拷贝:
//自己写拷贝构造函数 解决浅拷贝带来的问题
Person(const Person &p) {
cout << "Person拷贝函数的调用" << endl;
m_Age = p.m_Age;
//深拷贝操作
m_Height = new int(*p.m_Height);
}
C++提供了初始化列表语法,用来初始化属性
语法:构造函数 () : 属性1(值1)属性2(值2)…{}
Person(int age, int score) :m_Age(age), m_Score(score) {
}
C++中的成员可以是另一个类的对象,称为成员对象
class A{};
class B{
A a;
};
当其它类的成员作为本类成员,构造时先构造类对象,再构造自身;析构的顺序与构造相反
class Phone {
public:
string m_PName;
Phone(string pName) {
m_PName = pName;
cout << "Phone 构造函数调用" << endl;
}
~Phone() {
cout << "Phone 析构函数调用" << endl;
}
};
class Person {
public:
string m_Name;
Phone m_Phone;
Person(string name, string pname) :m_Name(name), m_Phone(pname) {
cout << "Person 构造函数调用" << endl;
}
~Person() {
cout << "Person 析构函数调用" << endl;
}
};
静态成员就是在静态成员变量和静态成员函数前加上static关键字
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为 静态成员变量分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了静态成员变量,也会影响到其他对象。
在C++中,类内的成员变量和成员函数是分开存储的
只有非静态成员变量才属于类的对象上
p占1个字节
空对象占用内存空间为1
C++编译器会给每个空对象分配1字节的空间,为了区分空对象占内存的位置
每个空对象也应该有一个独一无二的内存地址
class Person {
};
void test()
{
Person p;
cout << "size of p = " << sizeof(p) << endl;
}
p占4个字节
class Person {
int m_A; //非静态成员变量,属于类的对象上
static int m_B; //静态成员变量(类内声明,类外初始化),不属于类的对象上
void func() { //非静态成员函数,不属于类的对象上
}
static void func1() { //静态成员函数,不属于类的对象上
}
};
void test()
{
Person p;
cout << "size of p = " << sizeof(p) << endl;
}
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
问题:这块代码如何区分哪个对象在调用自己
C++提供特殊的对象指针this,解决上述问题
this指针指向被调用的成员函数所属的对象(谁调用this,this指向谁)
this指针是隐含每一个非静态成员函数内的一种指针(不需要传数据)
this指针不需要定义,直接使用即可
this指针的用途:
当形参和成员变量同名时,可用this指针来区分
在类的非静态成员函数中返回对象本身,可使用return *this
解决名称冲突
此时p1的年龄18不能够成功输出
class Person {
public:
int age;
Person(int age) { //编译器会认为三个age是同一个age,与成员属性age无关
age = age;
}
};
//解决名称冲突
void test0()
{
Person p1(18);
cout << "p1的年龄为:" << p1.age << endl;
}
利用this解决名称冲突问题
此时this指向的age与成员属性age一致
p1的年龄18可以正常输出
class Person {
public:
int age;
Person(int age) {
this->age = age;
}
};
返回对象本身用*this
class Person {
public:
int age;
Person(int age) { //编译器会认为9-10行中的三个age是同一个age,与7行的成员属性age无关
this->age = age;
}
Person& PersonAddAge(Person &p) { //用引用返回
this->age += p.age;
//this指向p3的指针,而*this指向的就是p2这个对象本体
return *this;
}
};
//返回对象本身用*this
void test1()
{
Person p2(10);
Person p3(10);
//链式编程思想
p3.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2);
cout << "p3 age = " << p3.age << endl;
}
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
class Person {
public:
void showClassName() {
cout << "this is Person class" << endl;
}
void showPersonAge() {
cout << "age = " << m_Age << endl;
}
int m_Age;
};
void test0()
{
Person *p = NULL;
p->showClassName(); //可以正常调用
p->showPersonAge(); //不可以正常调用
}
其中,showPersonAge函数实际上加上了this指针
void showPersonAge() {
cout << "age = " << this->m_Age << endl;
}
报错的原因是传入的为NULL指针,只要加入判断就可以避免
void showPersonAge() {
if (this->m_Age == NULL) {
return;
}
cout << "age = " << m_Age << endl;
}
常函数
成员函数后加const关键字
常函数内不可以修改成员属性
成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person {
public:
int m_A;
mutable int m_B; //特殊变量,即使在常函数中,也可以修改
//this指针的本质是指针常量,指针的指向不可以修改
//const Person * const this
//在成员函数后面加上const,修饰的是this指向,让指针指向的值也不可以修改
void func() const {
m_A = 100; //错误 等同于 this->m_A = 100;
m_B = 100; //正确 等同于 this->m_B = 100;
}
void func2() {
m_A = 10;
m_B = 10;
}
};
常对象
声明对象前加const称为常对象
常对象只能调用常函数
void test0()
{
const Person p; //在对象前加const,变为常对象
p.m_A = 100; //错误
p.m_B = 100; //正确
//常对象只能调用常函数
//常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
p.func(); //错误
p.func2(); //正确
}
在程序里,有些私有属性,想让类外的一些函数或者类进行访问,需要用到友元技术(客厅public卧室private和好朋友friend)
友元的目的就是让一个函数或者类,访问另一个类中的私有成员
友元的关键字:friend
友元的三种实现:
1.全局函数做友元
class Building {
//goodFriend全局函数是building类的友元,可以访问Building中私有成员
//只要写在类的最上面就行
friend void goodFriend(Building *building);
public:
string m_Livingroom; //客厅
private:
string m_Bedroom; //卧室
public:
Building() {
m_Livingroom = "客厅";
m_Bedroom = "卧室";
}
};
//全局函数
void goodFriend(Building *building)
{
cout << "好朋友全局函数,正在访问:" << building->m_Livingroom << endl;
cout << "好朋友全局函数,正在访问:" << building->m_Bedroom << endl;
}
void test0()
{
Building b;
goodFriend(&b);
}
2.类做友元
class Building; //类声明
class GoodFriend {
public:
Building *building;
GoodFriend();
void visit(); //参观函数,访问Building中的属性
};
class Building {
friend class GoodFriend;
public:
string m_Livingroom; //客厅
private:
string m_Bedroom; //卧室
public:
Building();
};
//类外写成员函数
Building::Building()
{
m_Livingroom = "客厅";
m_Bedroom = "卧室";
}
GoodFriend::GoodFriend()
{
//创建建筑物对象
building = new Building;
}
void GoodFriend::visit()
{
cout << "好朋友全局函数,正在访问:" << building->m_Livingroom << endl;
cout << "好朋友全局函数,正在访问:" << building->m_Bedroom << endl;
}
void test0()
{
GoodFriend gf;
gf.visit();
}
3.成员函数做友元
class Building;
class GoodFriend {
public:
GoodFriend();
void visit(); //让visit函数可以访问Building中私有成员
void visit2(); //visit2函数不可以
Building *building;
};
class Building {
//GoodFriend类下的visit函数作为本类的好朋友,可以访问私有的成员
friend void GoodFriend::visit();
public:
string m_Livingroom; //客厅
private:
string m_Bedroom; //卧室
public:
Building();
};
GoodFriend::GoodFriend()
{
building = new Building;
}
void GoodFriend::visit()
{
cout << "visit函数正在访问:" << building->m_Livingroom << endl;
cout << "visit函数正在访问:" << building->m_Bedroom << endl;
}
void GoodFriend::visit2()
{
cout << "visit函数正在访问:" << building->m_Livingroom << endl;
}
Building::Building()
{
m_Livingroom = "客厅";
m_Bedroom = "卧室";
}
void test0()
{
GoodFriend g;
g.visit();
g.visit2();
}
对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
class Person {
public:
int m_A;
int m_B;
//成员函数重载+号
//本质:Person p3 = p1.operator+(p2);
Person operator+ (Person &p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
};
全局函数重载
//全局函数重载+号
//本质:Person p3 = operator+(p1, p2);
Person operator+ (Person &p1, Person &p2)
{
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
//运算符重载也可以函数重载
Person operator+ (Person &p1, int i)
{
Person temp;
temp.m_A = p1.m_A + i;
temp.m_B = p1.m_B + i;
return temp;
}
注意:
1.对于内置的数据类型的表达式的运算符是不可能改变的
2.不要滥用运算符重载
作用:可以输出自定义的数据类型
同常不会用成员函数重载左移运算符,因为cout在左侧
//operator<<(cout) 简化版本:p << cout;
void operator<< (cout){}
只能利用全局函数左移运算符
void operator<<(ostream &cout, Person &p) //本质:operator<<(cout, p) 简化为cout << p;
{ //ostream 输出流,cout只能有一个,所以用引用的方式
cout << "m_A = " << p.m_A << "m_B = " << p.m_B;
}
void test0()
{
Person p;
cout << p;
}
如果使用上述方法进行左移运算符重载,不能进行“追加”
cout << p; //正确
cout << p << endl; //错误
这种“追加”的思想是链式编程的思想,返回值应该和cout一致
ostream & operator<<(ostream &cout, Person &p) //本质:operator<<(cout, p) 简化为cout << p;
{ //ostream 输出流,cout只能有一个,所以用引用的方式
cout << "m_A = " << p.m_A << "m_B = " << p.m_B;
return cout;
}
不能用编译器提供的赋值(类似于浅拷贝)原因:与深浅拷贝相同,堆区内存的重复释放,导致程序崩溃
class Person {
public:
int *m_Age;
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;
}
};
void test0()
{
Person p1(18);
Person p2(20);
Person p3(22);
p3 = p2 = p1;
cout << "p1的年龄为" << *p1.m_Age << endl;
cout << "p2的年龄为" << *p2.m_Age << endl;
cout << "p3的年龄为" << *p3.m_Age << endl;
}
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
class Person {
public:
string m_Name;
int m_Age;
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 true;
}
return false;
}
};
由于重载后使用的方式和函数的调用非常相似,因此称为仿函数
仿函数没有固定写法,非常灵活
class MyPrint {
public:
//重载函数调用运算符
void operator()(string test) {
cout << test << endl;
}
};
void test0()
{
MyPrint mp;
mp("HelloWorld");
}
class MyAdd {
public:
int operator()(int num1, int num2) {
return num1 + num2;
}
};
void test1()
{
MyAdd ma;
int ret = ma(100, 20);
cout << ret << endl;
//匿名对象
cout << MyAdd()(100, 20) << endl;
}