C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
封装
封装的意义
封装是C++面向对象三大特性之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装意义一:
在设计类的时候,属性和行为写在一起,表现事物。
语法:class 类名{ 访问权限:属性/行为};
补充:
- 类中的属性和行为我们统一称为成员
- 属性 又称为 成员属性 或 成员变量
- 行为 又称为 成员函数 或 成员方法
封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制。
访问权限有三种:
1、public 公共权限:成员,类内可以访问,类外也可以访问
2、protected 保护权限:成员,类内可以访问,类外不可以访问
3、private私有权限:成员,类内可以访问,类外不可以访问
struct和class的区别
在C++中,struct和class唯一的区别就在于默认的访问权限不同区别:struct默认权限为公共,class默认权限为私有
成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
class Person{
public:
//写姓名
void setName(string name){
my_name=name;
}
//读姓名
string getName(){
return my_name;
}
//读年龄
int getAge(){
return my_age;
}
//写情人
void setLover(string lover){
my_lover=lover;
}
private:
//姓名 设置权限为可读可写
string my_name;
//年龄 设置权限为只读
int my_age;
//情人 设置权限为只写
string my_lover;
};
类的分文件编写
类的分文件编写一般有4个步骤:
1、创建后缀名为.h的头文件
2、创建后缀名为.cpp的源文件
3、在头文件中写类的声明
4、在源文件中写成员函数的实现,并且添加上.h头文件的文件名
对象的初始化和清理
构造函数和析构函数
对象的初始化和清理是两个非常重要的安全问题 ,c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,但是编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(形参){ }
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名( ){ }
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
class Preson{
public:
//构造函数
//如果不写Person()构造函数,编译器也会自动写,只是函数内部是空实现
Person(){
cout<<"Person 构造函数的调用"<<endl;
}
//析构函数
~Preson(){
cout<<"Person 析构函数的调用"<<endl;
}
}
int main(){
//创建一个对象p,但是表面上并没有调用Person()构造函数
//然而执行结果却是,调用了构造函数(无参构造)。这是因为构造函数是自动被调用的
//注意:调用的是没有形参的构造函数
Person p;
}
构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造(又称 默认构造)
- 按类型分为:普通构造和拷贝构造
三种调用方式:
1、括号法 2、显示法 3、隐式转换法
class Person{
public:
int my_age;
//无参构造
Person(){
cout<<"Person() 无参构造函数的调用"<<endl;
}
//有参构造
Person(int age){
my_age=age;
cout<<"Person(int age) 有参构造函数的调用"<<endl;
}
//拷贝构造:把对象p复制过来
//为什么形参前加上const:因为拷贝对象p,而不能修改p,所以加上const保护
//为什么以引用的方式:节省内存空间
Person(const Person &p){
//将传入的人身上的所有属性,拷贝到我身上
my_age=p.my_age;
cout<<"Person(const Person &p) 拷贝构造函数的调用"<<endl;
}
~Person(){
cout<<"~Person() 析构函数的调用"<<endl;
}
}
int main(){
//1、括号法
Person p1; //默认构造函数的调用
Person p2(18); //有参构造函数的调用
Person p3(p1); //拷贝构造函数的调用
//注意事项1
//调用默认构造函数时候,不要加()
//因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
//Person p1(); //error
//2、显示法
Person p1; //默认构造函数的调用
Person p2=Person(24); //有参构造函数的调用
Person p3=Person(p2); //拷贝构造函数的调用
//注意事项2
//Person(24)是匿名对象
//匿名对象的特点:当前行执行结束后,系统会立即回收掉匿名对象
//注意事项3
//不要利用拷贝构造函数初始化匿名对象
//因为编译器会认为Person(p3) == Person p3,是个对象声明
//Person(p3); //error
//3、隐式转换法
Person p4=30; //有参构造函数的调用,相当于Person p4=Person(30)
Person p5=p2; //拷贝构造函数的调用
}
拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
class Person{
public:
int my_age;
Person(){
cout<<"Person() 无参构造函数的调用"<<endl;
}
Person(int age){
my_age=age;
cout<<"Person(int age) 有参构造函数的调用"<<endl;
}
Person(const Person &p){
my_age=p.my_age;
cout<<"Person(const Person &p) 拷贝构造函数的调用"<<endl;
}
~Person(){
cout<<"~Person() 析构函数的调用"<<endl;
}
}
//2、值传递的方式给函数参数传值
//实参传递给形参p时,会调用构造函数,拷贝一份给形参p
void func(Person p){
p.my_age=24;
}
//3、值方式返回局部对象
//返回局部变量p时,会调用构造函数,拷贝一份给接受的对象p3
Person test(){
Person p;
return p;
}
int main(){
Person p1(18);
Person p2(p1); //1、使用一个已经创建完毕的对象来初始化一个新对象
func(p2);
Person p3=test();
}
构造函数的调用规则
默认情况下,C++编译器至少给一个类添加3个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
深拷贝和浅拷贝
深拷贝:在堆区重新申请空间,进行拷贝操作
浅拷贝:简单的赋值拷贝操作
class Person{
public:
int my_age;
int *my_heigh;
Person(){
cout<<"Person() 无参构造函数的调用"<<endl;
}
Person(int age,int heigh){
my_age=age;
//height是个形参,函数调用结束后就会释放,所以不能写成my_heigh=&height
//new一个空间,用来存放int型数据height,并且把地址给my_heigh
my_heigh=new int(heigh);
cout<<"Person(int age) 有参构造函数的调用"<<endl;
}
~Person(){
//析构代码,将堆区开辟数据做释放操作
if(my_heigh!=NULL){
delete my_heigh;
my_heigh=NULL; //为避免野指针出现,给指针置为空
}
cout<<"~Person() 析构函数的调用"<<endl;
}
};
int main(){
//利用有参构造,创建对象p1
Person p1(18,180);
//利用拷贝构造,创建对象p2
Person p2(p1);
}
上述代码运行结果:
原因:
浅拷贝就是把数据逐字节地copy过来。构造对象p2时,首先把m_Age复制过来,所以p2的m_Age也是18;然后把m_Height也复制过来,这个数据存储的是个地址,因此p1和p2的m_Height同时指向堆区的0x0011地址。
分析程序运行过程:首先利用有参构造创建p1,然后用编译器提供的拷贝构造创建p2。程序执行结束,先析构p2,会把堆区0x0011地址释放,再把p2中的m_Height修改为NULL;然后析构p1,p1的m_Height仍然还是0x0011,所以也会做一次delete的操作,然而此地址已经被释放,属于违法操作。
编译器提供的拷贝构造函数:
浅拷贝带来的问题:堆区的内存重复释放
如何解决?
浅拷贝的问题要利用深拷贝进行解决:也就是在堆区再申请一个区域,存放身高height。即p1的m_Height指向堆区的0x0011地址,p2的m_Height指向堆区的0x0022地址
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。也要注意在析构函数中释放。
初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1) ,属性2(值2)... {};
class Person{
public:
string name;
int age;
//初始化列表初始化属性
//相当于默认构造
Person():name("牛帅涵"),age(24){
}
//初始化列表初始化属性
//相当于有参构造
Person(string n,int a):name(n),age(a){
}
}
int main(){
Person p;
cout<<p.name<<endl;
cout<<p.age<<endl;
Person p1("hou",23);
cout<<p1.name<<endl;
cout<<p1.age<<endl;
}
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
class Phone{
public:
string p_name;
Phone(string name){
p_name=name;
cout<<"Phone的构造函数调用"<<endl;
}
~Phone() {
cout << "Phone的析构函数调用" << endl;
}
};
class Person{
public:
string m_name;
Phone m_p;
//m_p(pName)等价于Phone m_p=pName 隐式转化法
Person(string name,string pName):m_name(name),m_p(pName){
cout<<"Person的构造函数调用"<<endl;
}
~Person() {
cout << "Person的析构函数调用" << endl;
}
};
int main(){
Person("张三","苹果MAX");
}
代码执行结果:
总结:当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序刚好相反。
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。
静态成员分为两种:
1、静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存,分配在全局区
- 类内声明,类外初始化
class Student {
public:
//类内声明
static string school;
};
//类外初始化
//这里不需要加static关键字,写明数据类型,标记属于Student类的区域
//如果不标记区域,则系统会将school认为是全局变量
string Student::school = "北京大学";
int main() {
Student s1;
cout << s1.school << endl;
//静态成员变量不属于某个对象上,所有对象都共享同一份数据,因此静态成员变量有两种访问方式:
//1、通过对象进行访问
Student s2;
s2.school = "清华大学";
cout << s1.school << endl;
cout << s2.school << endl;
cout<<Student::school<<endl; //2、通过类名进行访问
}
执行代码结果(因为p1和p2共享同一份数据):
静态成员变量访问权限:
2、静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
class Student {
public:
//静态成员变量 类内声明
static string school;
int age;
//静态成员函数
static void showSchool() {
cout << school << endl; //静态成员函数只能访问静态成员变量
//cout << age << endl; //静态成员函数 不可以访问 非静态成员变量
}
};
//类外初始化
string Student::school = "北京大学";
int main() {
//静态成员函数有两种调用方式
Student s1;
s1.showSchool(); //1、通过对象访问
Student::showSchool(); //2、通过类名访问
}
执行结果:
静态成员函数也是有访问权限的,同上!!!
C++对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的具体某一个对象上(静态成员属于所有的对象)。
1、类中无 成员变量和成员函数
class Person{
};
int main(){
Person p;
cout<<sizeof(p)<<endl; //空对象占用内存空间为:1
//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
//可以创造很多个空对象,每个空对象都占独一无二的地址
}
2、类中有非静态成员变量
class Person{
public:
int age; //非静态成员变量
};
int main(){
Person p;
cout<<sizeof(p)<<endl; //占用内存空间为:4
//非静态成员变量才属于类的具体某一个对象上
//编译器不需要给他另开一个1B来做区分
}
3、类中有静态成员变量、非静态成员函数和静态成员函数
class Person{
public:
int age; //非静态成员变量
static string school; //静态成员变量
void showAge(){ //非静态成员函数
cout<<age<<endl;
}
static void showSchool(){ //静态成员函数
cout<<school<<endl;
}
};
string Person::school="=北京大学";
int main(){
Person p;
cout<<sizeof(p)<<endl; //占用内存空间为:4
//静态成员变量、非静态成员函数和静态成员函数 不属于类的具体某一个对象
}
this指针
上述我们知道在C++中,成员变量和成员函数是分开存储的。每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。那么问题是:这一块代码是如何区分哪个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象。
this指针是隐含在每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可。
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
class Person {
public:
int age;
Person(int age) {
//this指针指向被调用的成员函数所属的对象
this->age = age;
}
//返回对象本体要用引用的形式
Person& addAge(const Person& p) {
age += p.age;
//*this表示调用addPerson成员函数的对象
return *this;
}
};
int main() {
Person p(10);
Person p1(10);
//链式编程思想,由于addAge函数返回的是对象本体,所以仍然可以再次调用addAge函数
p1.addAge(p).addAge(p).addAge(p);
//返回值30
/*
如果addAge函数的返回值不是Person引用,而是Person的话,返回值为20:
addAge函数返回的不是对象本体,p1.addAge(p)相当于,Person temp=p1.addAge(p);
也就是说返回之后,会创建一个临时变量temp接收
而temp和p1是两个独立的对象,只是成员变量age值相同而已。
*/
cout << p1.age << endl;
}
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
class Person {
public:
int age;
void show() {
cout << "这是一个人类" << endl;
}
void showAge() {
//如果是个空指针调用showAge函数,那么会访问null->age,肯定会报错
cout << age << endl; //编译器会默认成this->age
}
void showage() {
//保证代码的健壮性
if (this == NULL) {
return;
}
cout << age << endl;
}
};
int main() {
Person* p = NULL;
p->show(); //可以正常调用
//p->showAge(); //用到this指针,不能调用
p->showage(); //可以正常调用
}
const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数,修改mutable修饰的成员变量
class Person {
public:
int age;
mutable int height; //特殊变量,即使在常函数中,也可以修改这个值
//为什么常函数不能修改成员变量的值?
/*
age=10等价于this->age=10,而this指针是个 指针常量,即Person * const this;
指针常量:指针的指向是不可以修改的,但是指针指向的内容可以修改
如果使this指针指向的内容也不可以修改,只需把this定义成const Person * const this;
而在成员函数后加const,就相当于const Person * const this
*/
void show() const{
//age = 10; //this指针指向的内容不可以修改
//this = NULL; //this指针不可以修改指针的指向
height = 180;
}
void func() {
age = 24;
}
};
int main() {
//常对象
const Person p;
//p.age = 18;
p.height = 180; //m_B是特殊值,在常对象下也可以修改
p.show();
/*
常对象不可以调用普通成员函数,因为普通成员函数可以修改属性,
若是允许调用,则相当于变相修改成员属性
*/
//p.func();
}
友元
生活中你家有客厅(Public),有你的卧室(Private)。客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。友元的目的就是让一个函数或者类访问另一个类中私有成员。友元的关键字为friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
//房子类
class Building {
//goodGay全局函数是 Building类的好朋友,可以访问Building中私有成员
//写在类的最上面,不需要注明访问权限
friend void goodGay(Building& b);
public:
string sittingRoom;
Building() {
sittingRoom = "客厅";
bedRoom = "卧室";
}
private:
string bedRoom;
};
//全局函数
void goodGay(Building& b) {
cout << "好基友全局函数,正在访问: " << b.sittingRoom << endl;
//访问私有成员
cout << "好基友全局函数,正在访问: " << b.bedRoom << endl;
}
int main() {
Building b;
goodGay(b);
}
类做友元
//房子类
class Building {
//GoodGay类是Building类的好朋友,可以访问Building类中私有成员
friend class goodGay;
public:
string sittingRoom;
Building(); //成员函数声明,在类外实现
private:
string bedRoom;
};
class goodGay {
public:
Building* b;
void visit(); //参观函数访问Building中的属性
goodGay() {
//创建房子对象
b = new Building;
}
};
//在类外实现成员函数
Building::Building() {
sittingRoom = "客厅";
bedRoom = "卧室";
}
void goodGay::visit() {
cout << "好基友类,正在访问: " << b->sittingRoom << endl;
//访问私有成员
cout << "好基友类,正在访问: " << b->bedRoom << endl;
}
int main() {
goodGay g;
g.visit();
}
成员函数做友元
//房子类
class Building {
//告诉编译器,goodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
friend void goodGay::visit();
public:
string sittingRoom;
Building() {
sittingRoom = "客厅";
bedRoom = "卧室";
}
private:
string bedRoom;
};
class goodGay {
public:
Building* b;
//让visit函数可以访问Building中私有成员
void visit() {
cout << "visit函数正在访问: " << b->sittingRoom << endl;
cout << "visit函数正在访问: " << b->bedRoom<< endl;
}
//让visit2函数不可以访问Building中私有成员
void visit2() {
cout << "visit2函数正在访问: " << b->sittingRoom << endl;
cout << "visit函数正在访问: " << b->bedRoom << endl;
}
goodGay() {
b = new Building;
}
};
int main() {
goodGay g;
g.visit();
}
运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
加号运算符重载
作用:实现两个自定义数据类型相加的运算。有两种重载方式:
1、成员函数重载+号
class Person {
public:
int m_a;
int m_b;
Person() {
m_a = m_b = 0;
}
Person(int a, int b) {
m_a = a;
m_b = b;
}
//1、成员函数重载+号,函数名称必须写成operator+
Person operator+(Person& p) {
Person temp(m_a + p.m_a, m_b + p.m_b);
return temp;
}
};
int main() {
Person p1(10, 10);
Person p2(10, 20);
//等价于Person p3 = p1.operator+(p2);这是成员函数重载+的本质
Person p3 = p1 + p2; //简化形式
cout << p3.m_a << endl;
cout << p3.m_b << endl;
}
2、全局函数重载+号
class Person {
public:
int m_a;
int m_b;
Person() {
m_a = m_b = 0;
}
Person(int a, int b) {
m_a = a;
m_b = b;
}
};
//全局函数重载+号
Person operator+(Person& p1, Person& p2) {
Person temp(p1.m_a + p2.m_a, p1.m_b + p2.m_b);
return temp;
}
//运算符重载,也可以发生函数重载
Person operator+(Person &p,int a) {
Person temp(p.m_a + a, p.m_b + a);
return temp;
}
int main() {
Person p1(10, 10);
Person p2(10, 20);
//等价于Person p3 = operator+(p1,p2);这是全局函数重载+号的本质
Person p3 = p1 + p2;
cout << p3.m_a << endl;
cout << p3.m_b << endl;
//本质是Person p4 = operator+(p1,10);
Person p4 = p1 + 10;
cout << p4.m_a << endl;
cout << p4.m_b << endl;
}
左移运算符重载
继承
多态