C++学习——类和对象
具有相同性质的对象,可以抽象为类
1.1封装
1.1.1封装的意义
-
将属性和行为作为一个整体,表现生活中的事物
-
将属性和行为加以权限控制
访问权限有三种:- public 公共权限,类内可以访问,类外可以访问
- protected 保护权限,类内可以访问,类外不可以访问(子类可以访问父类中的保护内容)
- private 私有权限,类内可以访问,类外不可以访问(子类不可以访问父类中的私有内容)
class 类名
{
//访问权限
public://公共权限
protected://保护权限
private://私有权限
//类中的属性和行为,称为成员。类中可以让另一个类作为本类中的成员
//属性(用变量)(成员属性、成员变量)
//行为(用函数)(成员函数、成员方法)
};
实例化:通过一个类,创建一个对象的过程。
1.1.2 strcut与class的区别
- 唯一的区别在于默认的访问权限不同
- struct默认权限为公共
- class默认权限为私有
1.1.3成员属性设置为私有
- 优点1:将所有成员属性设置为私有,可以自己控制读写权限
- 优点2:对于写权限,我们可以检测数据的有效性
操作方法是通过声明public的函数(方法)作为功能接口,实现控制读写权限/检测数据的有效性
class Person
{
public:
//写操作
void setName(string setname)
{
name = setname;
}
private:
string name;
string lover;
};
1.2对象的初始化和清理
1.2.1构造函数与析构函数
- 一个对象或者变量没有初始状态,对其使用后结果未知
- 使用完一个对象或者变量,没有及时清理,也会造成安全问题
C++利用构造函数和析构函数来解决上述问题,这两个函数会被编译器自动调用,完成对象的初始化和清理。如果我们不提供构造函数和析构函数,编译器会提供,但是是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用。
- 语法:
类名(){}
- 没有返回值也不写void
- 函数名称与类名相同
- 可以有参数,因此可以重载
- 程序在调用对象时会自动调用,而且只会调用一次
- 语法:
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
- 语法:
~类名(){}
- 没有返回值也不写void
- 函数名与类名相同,前面加上~
- 不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用,且只会调用一次
- 语法:
1.2.2构造函数的分类及调用
- 分类
- 按参数分为:有参构造和无参构造(默认构造)
- 按类型分为:普通构造和拷贝构造
拷贝构造函数:
类名(const 类名 &变量名){}
- 调用方式
- 括号法
注:调用无参构造函数时不能加(),否则会被编译器当成函数声明 - 显示法
类名 变量名;
类名 变量名 = 类名(参数);
//有参构造类名 变量名 = 类名(变量名)
//拷贝构造- 注:
类名(参数)
这是一个匿名对象,当前行执行结束后系统会自动收回匿名对象。不要用拷贝构造来初始化匿名对象
- 隐式转换法
- 括号法
1.2.3拷贝构造函数的调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
1.2.4构造函数的调用规则
默认情况下,C++编译器至少给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则
- 如果用户定义有参构造,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++就不会提供其他构造函数
1.2.5深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作。带来的问题就是堆区内存的重复释放
默认拷贝构造函数就会进行浅拷贝,如果一个指针指向堆区,那么该堆区会被两个指针指向。最后释放内存时就会导致堆区内存的重复释放。
- 深拷贝:在堆区重新申请空间,进行拷贝操作
自己写一个拷贝构造函数实现,对于指针的拷贝,不能直接用指针赋值(浅拷贝如此做),而是要重新开一段堆区让指针指向它。
m_Height = new int(*p.m_Height);
(案例)
1.2.6初始化列表
语法:构造函数():属性1(值1),属性2(值2)…{}
class Person
{
public:
//传统的初始化方式
Person(int a, int b, int c){
m_A = a;
m_B = b;
m_C = c;
}
//初始化列表方式初始化
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;
};
1.2.7类对象作为类成员
C++类中成员可以使另一个类的对象,称该成员为对象成员
当其他类对象作为本类成员,构造时先构造类对象,再构造自身;析构的顺序与构造相反。
1.2.8静态成员
静态成员就是在成员变量和成员函数前加关键字static
- 静态成员变量(也是有访问权限的)
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 两种访问方式
- 通过对象进行访问
Person p; p.m_A;
- 通过类名进行访问
Person::m_A;
- 通过对象进行访问
- 静态成员函数(也是有访问权限的)
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 两种访问方式
- 通过对象进行访问
Person p; p.func();
- 通过类名进行访问
Person::func();
- 通过对象进行访问
1.3C++对象模型和this指针
1.3.1成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上(体现在sizeof(类的对象))
C++编译器会为每个空对象分配一个字节空间,为了区分空对象占内存的位置
1.3.2this指针
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用
this指针是指针常量,指向不可修改
- this指针的用途:
- 当形参和成员变量同名时,可用this指针区分
- 在类的非静态成员函数中返回对象本身,可使用
return *this
class Person { public: Person(int age) { this->age = age; } Person & PersonAddAge(Person p) { this->age += p.age; //返回对象本身 return *this; } int age; }; Person p1(10); Person p2(10); //链式编程思想 p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
1.3.3空指针访问成员函数
C++中空指针是可以调用成员函数的,但是要注意是否用到this指针,如果用到this指针,需要加以判断保证代码(判断是否为NULL)的健壮性
1.3.4const修饰成员函数
-
常函数
- 成员函数后加const之后的函数称为常函数。
void showPerson() const {}
,const修饰的是this指向,让指针指向的值也不可以修改。 - 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
- 成员函数后加const之后的函数称为常函数。
-
常对象
- 声明对象前加const后的对象称为常对象
const Person p;
- 常对象只能调用常函数
- 声明对象前加const后的对象称为常对象
1.4友元
在程序里,有些私有的属性想让类外特殊的一些函数或者类进行访问,需要用到友元。
友元的目的是让一个函数或者类访问另一个类中的私有成员
友元的关键字为friend
- 友元的三种实现方法
- 全局函数作友元
friend void goodgay(Building *building);
放在类中可以告诉编译器goodgay可以访问类中的私有内容 - 类作友元
friend class goodgay;
- 成员函数作友元
friend void GoodGay::visit();
- 全局函数作友元
1.5运算符重载
运算符重载:对已有运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
1.5.1加号运算符重载
实现两个自定义数据类型相加的运算
- 通过成员函数重载
//在类内的成员函数 Person operator+(const 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 operator+(const Person &p1, const Person &p2){ Person temp(0,0); temp.m_A = p1.m_A + p2.m_A; temp.m_B = p1.m_B + p2.m_B; return temp; }
1.5.2左移运算符重载
通常不会利用成员函数实现左移运算符,因为无法实现cout在<<左侧
ostream & operator<<(ostream &cout, Person &p){
cout << "m_A= " << p.m_A << "m_B= " << p.m_B;
return cout;
}
1.5.3函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定的写法
1.6继承
1.6.1继承的基本语法
class 子类: 继承方式 父类
,如class A: public B
子类也称为派生类,派生类中的成员包含两大部分:①从基类继承过来的②自己增加的成员
父类也称为基类
1.6.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;
}
1.6.3继承中的对象模型
父类中的所有非静态成员属性都会被子类继承下去
父类中的私有成员属性,是被编译器隐藏了,因此是访问不到,但是确实被继承
1.6.4继承中构造和析构顺序
先调用父类的构造函数再调用子类的构造函数,析构的顺序相反
1.6.5继承同名成员的处理方式
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域(即:: )
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数。如果想访问到父类中被隐藏的同名成员函数,需要加作用域。
1.6.6继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域(即:: )
1.6.7多继承语法
C++允许一个类继承多个类
语法:class 子类: 继承方式 父类1, 继承方式 父类2, ···
多继承可能会引发父类中有同名成员出现,需要加作用域区分
1.6.8菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承称为菱形继承
利用虚继承可以解决菱形继承的问题,在继承之前加上关键字virtual
1.7多态
1.7.1多态的基本概念
- 多态可分为两类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
- 动态多态满足:①有继承关系②子类重写父类的虚函数(重写是指函数返回值、函数名、参数列表均相同)
- 动态多态的使用:父类的指针或者引用 指向子类对象
- 静态多态与动态多态的区别
- 静态多态的函数地址早绑定–编译阶段确定函数地址
- 动态多态的函数地址晚绑定–运行阶段确定函数地址
- 多态带来的好处
- 组织结构清晰
- 可读性强
- 对于前期后期的扩展和可维护性高
1.7.2纯虚函数和抽象类
在多态中,父类中虚函数的实现通常无意义,主要调用子类重写的内容。因此可以将虚函数改为纯虚函数。类中有了纯虚函数,则该类称为抽象类
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
- 抽象类特点
- 无法实例化对象
- 子类必须重写抽象类的纯虚函数,否则也属于抽象类
1.7.3虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
- 虚析构和纯虚析构的共性
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
- 虚析构和纯虚析构的区别
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){};
纯虚析构语法:virtual ~类名() = 0;