本文主要是介绍[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
我们知道C++相对于C语言最大的提升便是加入了面向对象这一特性,本文将对面向对象的所有特性进行说明。并且以下文章中,所有示例及结果的实验环境为 win10-64,实验环境为为visual stdio2019
目录
- 1. 什么是面向对象?
- 1.1 类和对象的引入
- 2. 什么是类及类相关知识
- 2.1 C语言与C++结构体的区别
- 2.2 class关键字
- 2.3 类的两种定义形式
- 2.3.1 声明定义一起实现
- 2.3.2 声明定义分文件处理
- 2.4 类的访问限定符与封装
- 2.4.1 封装
- 2.4.2 访问限定符
- 2.4 类的作用域
- 2.5 类的实例化
- 2.5 类如何在内存中存储(通过计算对象的大小观察)
- 2.5.1 对类存储模型的三种猜想
- 2.5.2 给定一个空类,它的大小是多少
- 2.5.3 结论
- 3. this指针
- 3.1 问题提出
- 3.2 this指针的作用
- 3.3 this指针的特性
- 4. 默认的成员函数
- 4.1 类的6个默认成员函数
- 4.2 构造函数
- 4.2.1 作用及用法
- 4.2.2 特性
- 4.3 初始化列表
- 4.3.1 初始化列表的使用
- 4.3.1 注意事项
- 4.4 析构函数
- 4.4.1 功能
- 4.4.2 特性
- 4.5 拷贝构造函数
- 4.5.1 功能
- 4.5.2 特征
- 4.5.3 什么时候会调用拷贝构造函数
- 4.6 运算符重载
- 4.6.1 功能
- 4.6.2 运算符重载的注意事项
- 4.6.3 赋值运算符重载 '=' (重要)
- 4.6.4 取地址运算符重载
- 4.6.5 其他运算符的重载示例
- 5. const成员
- 5.1 const修饰成员变量
- 5.2 const修饰成员函数
- 5.2.1 概念及使用
- 5.2.2 对取地址运算符的重新重载
- 6. static成员
- 6.1 概念与初始化
- 6.2 修饰成员变量与成员函数的特性
- 6.2.1 修饰成员变量
- 6.2.1 修饰成员函数
- 7. 友元
- 7.1 友元函数
- 7.1.1 友元函数的引入
- 7.1.2 友元函数的特征
- 7.2 友元类
- 8. 内部类
- 总结
1. 什么是面向对象?
在学习C语言阶段,我们编写程序关心的是过程,分析求解问题的步骤,通过函数的调用一步步解决问题,这是C语言面向过程的思想。
现在C++语言引入了面向对象的思想,我们在编写程序时,不再单单只关注过程,而是关注我们操作的这个对象都有什么功能,都可以完成一些什么事情,依靠对象之间的交互完成一些功能的实现。
- 面向对象是一种对现实世界理解和抽象的方法,通过两部分进行描述对象,属性+功能,表示一个具体的对象。
- 面向过程 是一种 以过程为中心 的编程思想。以该功能的实现过程为主要思想。
面向过程的举例(以把大象放入冰箱为例)
面向对象举例
通过上述的比较,在简单的程序中,面向对象更方便,但是如果我们在后期继续扩展,则面向对象的优势会进一步的显现出来。面向对象的继承、封装、多态的属性会在接下来的文章中详细介绍。
这篇文章很详细的阐述了面向对象的优势所在,点击:面向对象的优势 (转自leetcode)查看。
1.1 类和对象的引入
在使用面向对象解决问题时,首先需要有该对象,所以通过什么手段进行描述。
对象的组成:
1.属性 -->该对象的具体属性,比如上述冰箱的属性就会具有 门、容量、冷冻…
2.功能 -->该对象能完成的事情,比如冰箱关门、开门…
- 类:用来描述对象,是一种自定义类型
- 对象:是类的实体,有属性和行为
2. 什么是类及类相关知识
2.1 C语言与C++结构体的区别
1.在C语言中,结构体内部只能定义变量,而不能定义函数。所以C语言中的结构体没有面向对象的属性,将C语言结构体定义出来的变量就叫结构体变量。
struct Stu
{int id;char* name;int getName() // error{return this->id;}
};
2.而在C++中,结构体内部不仅可以定义变量,还可以定义函数,并且可以通过成员访问运算符访问成员函数与成员变量,所以它具有面向对象的属性,在C++中我们把使用struct
定义出来的变量叫做对象
。
struct Stu
{int id;char* name;int getName() // ok{return this->id;}
};// 以下操作都可以正常使用
cout << stu.id << stu.name << stu.getName() << endl;
2.2 class关键字
虽然在C++中使用struct关键字可以定义对象,但是在C++中一般不会去使用struct
去定义对象,而是使用class
关键字进行对象的定义。
class className
{// 类体:成员函数与成员变量(对象的属性与方法)
};
2.3 类的两种定义形式
2.3.1 声明定义一起实现
使用这种方式定义,编译器可能会将其当作内敛函数进行处理。
class Student
{
private:void showMessage(){cout << id << name << gender << endl;}public:int id;char name[32];char gender[2];};
2.3.2 声明定义分文件处理
推荐使用该方式。
// Student.h
class Student
{
private:void showMessage();public:int id;char name[32];char gender[2];};// Student.cpp
#include "Student.h"// 通过域运算符 '::' 进行成员函数的实现
void Student::showMessage()
{cout << id << name << gender << endl;
}
2.4 类的访问限定符与封装
2.4.1 封装
将数据和操作数据的方法进行有机结合,访问权限限定符
用来隐藏对象的属性和细节,仅对外公开接口来和对象进行交互。
封装的本质其实是管理:将我们不想让外界知道的细节进行隐藏,开放一些公有的成员函数,对成员进行合理的访问。防止外界通过不正当方式破坏内部数据。
2.4.2 访问限定符
C++通过访问限定符进行类的封装,使用访问限定符来决定将哪些接口提供给外界访问。
- public(公有): 修饰的成员在类外可以被直接访问(类中的成员函数一般通过这种方式开放)
- private(私有): 修饰的成员在类外不能直接被访问
- protected(保护): protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private
注意:
- 访问权限作用域从该访问限定符出现的位置开始知道下一个访问限定符出现时为止
- class的默认访问权限为private,struct的访问权限为public(因为C语言中没有私有权限,所以要兼容C语言,权限设置为public)
2.4 类的作用域
结论:一个类就是一个作用域,在访问类中的成员是需要使用域运算符::
。
class Student
{
private:void showSMessage();public:int id;char name[32];char gender[2];
};class Teacher
{
private:void showTMessage();public:int id;char name[32];char gender[2];
};// 通过域运算符 '::' 指定该成员属于哪个作用域
void Student::showSMessage()
{cout << id << name << gender << endl;
}void Teacher::showTMessage()
{cout << id << name << gender << endl;
}
2.5 类的实例化
使用类创建对象的过程,就成为类的实例化
- 类只是一个模型,描述了该模型,属性域方法,定义出一个类并没有分配实际的内存空间来存储。
- 一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类的成员变量。
// 定义类的代码就不看了,直接看如何实例化int main()
{// 实例化一个对象Student stu1;// 通过成员访问运算符访问类的成员stu1.id = 10;stu1.name = "张三";stu1.gender = "男";stu1.showSMessage();Student stu2, stu3; // 还可以实例化多个对象
}
2.5 类如何在内存中存储(通过计算对象的大小观察)
2.5.1 对类存储模型的三种猜想
1. 对象包含所有的成员
每新建一个对象,该对象中存储该类中的所有成员。每一个对象都是如此。
稍微思考一番,就觉得这种方法不可能,为什么?
如果每个对象都将类中的成员及成员函数存储一份,那么对象每个对象变得异常的大。如果该类有多个对象,每个对象的数据单独存储没问题,但成员函数中的代码都是相同的,如果每次都保存相同代码,造成的空间浪费是巨大的。
2. 成员函数放在类外,通过指针指向函数位置
这种存储方式貌似可行,解决了代码重用问题,并且缩小了空间。那我么写个程序测试以下,看是否与我们猜测一致。
#include <iostream>
using namespace std;class Student
{
public:void showMessage(){cout << id << "-" << name << "-" << gender << endl;}char* getName(){return name;}//private:int id;char name[32];char gender[2];
};int main()
{Student stu;stu.id = 10;strcpy(stu.name, "张三");strcpy(stu.gender, "男");stu.getName();stu.showMessage();cout << "sizeof(stu): " << sizeof(stu) << endl;cout << "sizeof(Student): " << sizeof(Student) << endl;return 0;
}
上述程序,如果按照函数只存储函数指针的方式我们猜测大小应该为
44
或者48
。但是!!!,结果是40
,至此我们已经得出了结论。
3. 对象中只包含了成员变量
通过以上两种猜想,我们可以确定对象的存储模型只存储了成员变量,那么在使用时对象如何去寻找成员函数。编译器在编译时就会确定成员函数存储在何种位置,不需要指定存储位置。
2.5.2 给定一个空类,它的大小是多少
class Test
{};
由结果可知,空类所占大小为1个字节
假设空类大小为0个字节,使用空类创建了多个对象,但这些对象都没有空间,则他们在内存中就无法标识,并且在一个地方存储。就无法正确表示出这个对象。所以一定要由一个字节进行占位。
2.5.3 结论
对象的存储方式为,对象只包含成员变量,并不包含成员函数。并且按照内存对齐
的方式进行存储。大小计算也与结构体的计算方式相同。空类占1个字节。
3. this指针
3.1 问题提出
1.在类中,为什么可以把成员变量放在成员函数下方定义,并且可以正常使用?
2.多个对象调用一个函数,该函数怎么区分对哪个对象操作?
3.2 this指针的作用
在上述的问题2中,如何多个对象调用一个函数,如何区分是哪个对象调用?
通过this指针解决该问题。即在编译期间C++编译器,对每个
非静态的成员函数
增加了一个隐藏的指针参数,让该指针指向当前对象,在函数体中所有的成员变量操作,都是通过这个指针去访问。只不过不需要用户对该指针进行传递,编译器自动完成。
在成员函数中可以通过this->XXX
访问成员变量
class Student
{
public:Student(int id, const char name[], const char gender[]){_id = id;strcpy(_name, name);strcpy(_gender, gender);}void showMessage(){cout << "this address: " << this << endl;cout << _id << "-" << _name << "-" << _gender << endl;// 也可以通过这样访问cout << this->_id << this->_name << endl;}private:int _id;char _name[32];char _gender[2];
};int main()
{Student stu1(1, "张三", "男");cout << "&stu1 = " << &stu1 << endl;stu1.showMessage();cout << endl;Student stu2(2, "李四", "女");cout << "&stu2 = " << &stu2 << endl;stu2.showMessage();return 0;
}
结果:
3.3 this指针的特性
- this指针的类型:
类类型* const
,因为this指针的指向不能被修改- 只能在成员函数内部使用
- this指针本质上是成员函数的形参,是对象调用参数时,将对象地址作为实参传递给this形参,所以对象不存储this指针
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过寄存器自动传递,不需要用户传递。
void showMessage(/*Student* this*/){cout << "this address: " << this << endl;cout << _id << "-" << _name << "-" << _gender << endl;// 也可以通过这样访问cout << this->_id << this->_name << endl;}
4. 默认的成员函数
4.1 类的6个默认成员函数
如果一个类中什么成员都没有,则称该类空空类。但是空类中真的什么都没有吗?在我们声明一个类时,不管我们是否向类中添加成员,都会生成下列的6个默认的成员函数。
- 初始化和清理:
1. 构造函数:完成成员变量的初始化
2. 析构函数:完成成员变量的清理- 拷贝复制:
1. 拷贝构造函数:使用同类对象初始化创建对象
2. 赋值运算符重载:把一个对象赋值给另一个对象- 取地址重载:
1. 普通对象的取地址
2. const对象取地址
4.2 构造函数
4.2.1 作用及用法
在类中,由于其中的成员变量具有封装的特性,所以无法直接对其中的成员赋值,所以要通过设置公有方法进行对私有属性的改变,这样很麻烦。在对象创建时就把对应的信息设置进去。
所以,构造函数就出现了,构造函数较特殊,函数名与类名相同,并且无返回值,创建类类型对象时由编译器自动调用,并且在该对象的声明周期中只会调用一次。如果没有定义构造函数,则编译器会自动生成一个无参的构造函数。
没有构造函数的对成员进行设值 每次新建对象都这样这样操作
class Stu
{
public:void SetId(int _id){id = _id;}void SetAge(int _age){age = _age;}private:int id;int age;
};int main()
{// 给对象设置值Stu s1;s1.SetAge(10);s1.SetId(1111);return 0;
}
利用构造函数设值 显然方便了许多
class Stu
{
public:Stu(int _id, int _age){id = _id;age = _age;}private:int id;int age;
};int main()
{// 新建对象时设值Stu s1(10, 123); Stu s2(11, 124);Stu s3(13, 125);return 0;
}
4.2.2 特性
1.构造函数的重载
注意:一旦自己改写了,构造函数,则编译器不会再生成默认的构造函数
如果有时候不需要对新建的对象设值,那么上面的方式就达不到我们的要求,但是构造函数的另一特性又出现了,可以对构造函数进行重载,满足编程时的不同要求。
class Stu
{
public:// 两个参数的构造函数Stu(int _id, int _age){id = _id;age = _age;}// 无参的构造函数Stu() {}// 一个参数的构造函数Stu(int _id){id = _id;}private:int id;int age;
};int main()
{// 利用重载创建不同对象Stu s1;Stu s2(1, 123);Stu s3(1);return 0;
}
2. 用户如果定义构造函数则不会自动生成无参构造函数
class Stu
{
public:// 两个参数的构造函数Stu(int _id, int _age){id = _id;age = _age;}private:int id;int age;
};int main()
{Stu s1; // err 因为找不到无参的构造函数Stu s2(1, 123);return 0;
}
3. 无参构造函数和全缺省的构造函数都是默认构造函数,只能有一个
class Stu
{
public:// 全缺省构造函数Stu(int _id = 1, int _age = 10){id = _id;age = _age;}// 无参构造函数Stu() {}private:int id;int age;
};int main()
{Stu s1; // 产生了二义性Stu s2(1, 123);return 0;
}
4. 如果类嵌套类,则在生成类对象时,会自动调用嵌套类的无参构造函数
5. 在构造函数中是对成员的赋值,不是对成员的初始化
4.3 初始化列表
在上述构造函数内部,我们给成员变量赋值,在那一部分,不是对成员变量的初始化,因为初始化只有一次,而函数体内部的赋值可以有多次。如果对成员变量进行初始化,就利用到了现在的初始化列表。
4.3.1 初始化列表的使用
class Stu
{
public:Stu(int _id = 1, int _age = 10): id(_id) // 初始化列表以 ':' 开始, age(_age) // 以 ',' 进行分隔{}private:int id;int age;
};
4.3.1 注意事项
1.类中包含以下成员则必须在初始化列表位置进行初始化
1.1 const成员变量
因为const成员变量无法在声明时进行赋值,但是const具有不可修改的特性,但是不能没有初始值,所以就要在初始化列表的位置进行初始化。
class Stu
{
public:Stu(int _id = 1, int _age = 10): id(_id), age(_age), status(1) // 对const成员变量进行初始化{}private:int id;int age;// const成员变量const int status;
};
1.2 引用成员变量
原因上同
class Stu
{
public:Stu(int _id = 1, int _age = 10): id(_id), age(_age), ref(_age){}private:int id;int age;// int& ref;
};int main()
{//Stu s1; // 产生了二义性Stu s2(1, 123);return 0;
}
1.3 没有默认构造函数的自定义类型
全缺省和无参构造函数都被称为默认构造函数
如果自定义类型有默认的构造函数,再创建母对象时,会调用自定义类型的无参构造函数。若没有默认的构造函数则必须要给类型传参数,否则无法成功建立对象,所以要在初始化列表的位置进行自定义类型的初始化。
class Test
{
public:Test(int _score): score(_score){}private:int score;
};class Stu
{
public:Stu(Test _test, int _id = 1, int _age = 10): id(_id), age(_age), test(_test) // 自定义类型初始化{}private:int id;int age;//自定义类型的成员 Test test;
};
1.4 成员变量的初始化顺序,是在类中的声明顺序,与初始化列表顺序无关
class Test
{
public:// 初始化列表的初始化顺序:// b -> c -> a// 因为先用b初始化的a,但a中为随机值,所以b中也是随机值Test(int _a = 1, int _b = 2, int _c = 3): a(_a) , b(a), c(_c){}void PrintRes(){cout << a << " " << b << " " << c << endl;}private:int b;int c;int a; // 注意这里 a 的位置
};int main()
{Test test;test.PrintRes();return 0;
}
4.4 析构函数
4.4.1 功能
析构函数的功能与构造函数功能恰恰相反,析构函数不是完成对象的销毁,而是完成对象中资源的清理的工作,对象在销毁时,会自动调用对象的析构函数,完成对象中资源的清理。如果在构造对象时,没有申请资源,则可以不去写析构函数。
class Stu
{
public:Stu(int _id, const char* _name): id(_id){cout << "构造函数" << endl;name = (char*)malloc(sizeof(char) * 16);strcpy(name, _name);}// 析构函数// 因为在构造阶段进行了资源的申请,则在对象销毁期间必须释放资源~Stu(){cout << "析构函数" << endl;free(name);}void Print(){cout << "id:" << id << " name:" << name << endl;}private:int id;char* name;
};int main()
{Stu s(1, "张三");s.Print();return 0;
}
4.4.2 特性
- 无参数无返回值
- 一个类只有一个析构函数,如果为定义,则系统会自动生成默认的析构函数
- 在对象生命周期结束时,会自动调用析构函数
4.5 拷贝构造函数
4.5.1 功能
在创建对象时,通过已有对象,创建出与其完全相同的新对象,称为拷贝构造函数。
拷贝构造函数,无返回值,只有单个形参,该形参是对本类类型的常引用,用已经存在的对象创建新对象,由编译器自动调用。
函数原型
class Test
{Test(const Test& test){// 函数体}
};
示例:
class Stu
{
public:// 构造函数Stu(int _id = 1, int _score = 2){id = _id;score = _score;}void Print(){cout << "id:" << id << " socre:" << score << endl;}private:int id;int score;
};int main()
{Stu s1(1, 100);s1.Print();// 调用系统的默认拷贝构造函数Stu s2(s1);s2.Print();// 输出结果完全相同return 0;
}
4.5.2 特征
1. 如果构造函数中有申请资源的行为,那么不可使用默认拷贝构造函数
这个程序会崩溃,为什么?
class Stu
{
public:// 构造函数Stu(int _id, const char* _name){id = _id;name = (char*)malloc(sizeof(char) * 16);strcpy(name, _name);}void Print(){cout << "this:" << this << " this.name:" << &(this->name) << endl;cout << "id:" << id << " socre:" << name << endl;}// 析构函数 释放资源~Stu(){cout << "析构函数" << endl;free(name);}private:int id;char* name;
};int main()
{Stu s1(1, "张三");s1.Print();// 调用系统的默认拷贝构造函数Stu s2(s1);s2.Print();return 0;
}
通过上述程序可以看出,通过拷贝构造函数构造出的对象,从对象地址可以看出不是同一个对象,但是对象中的name的空间是通过malloc动态申请得到的,系统默认的拷贝构造函数,在拷贝时,不会去申请地址空间,而是直接复制前一个对象的地址,共用一一份资源。
在销毁对象时,会调用析构函数,析构函数中对申请的资源进行了释放,因为共用一份资源,所以会对一块空间释放两次,造成了程序的崩溃。
2. 拷贝函数的参数只有一个且必须使用引用传参,使用传值则会发生无限递归
假设拷贝构造函数为传值
在参数传递时,需要生成一份实参的拷贝,因为是自定义类型,所以在生成实参的拷贝时,会调用拷贝构造函数。该过程又是一个拷贝实参的过程,所以又会调用拷贝构造,就这样一直递归下去。
4.5.3 什么时候会调用拷贝构造函数
- 用已经存在的对象创建新对象时
- 该类类型作为参数进行传递时,会调用拷贝构造函数
- 成员函数中,以值的方式进行返回时,在返回时需要创建一个临时的对象,所以会调用拷贝构造函数
4.6 运算符重载
4.6.1 功能
但我们自定义了一个类型,系统原本的操作符无法满足我们的操作。例如,定义了一个日期类,比较两日期大小,通过系统 < 或 >
无法比较自定义类型,可以写一个函数进行比较,但是可读性较差,所以引入运算符重载。
C++为了增强代码可读性引入了运算符重载,运算符重载时具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表。不能改变原有运算符的含义。
函数名:关键字 operator后接需要重载的运算符符号
函数原型:返回值类型 operater操作符(参数列表)
4.6.2 运算符重载的注意事项
- 不能通过连接其他符号来创建新的操作符
- 重载操作符,它的参数列表必须要有一个类类型或者枚举类型的操作数
为什么?如果不是自定类型,那么参数列表中的都一定是内置类型,操作符已经支持了内置类型的计算,不需要重载。如果重载了该操作符,则在这个重载函数内部会发生无限递归。- 重载内置类型的操作符,其含义不能改变
- 以下五个操作符不能进行重载
.*
、::
、sizeof
、?:
、.
4.6.3 赋值运算符重载 ‘=’ (重要)
例如,新建了两个对象,两个对象内容不同,如何使两个对象中的内容相同,那么就要通过赋值运算符将两个对象中的内容变得相同。
class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print(){cout << "id:" << id << " socre:" << score << endl;}// 赋值运算符重载Stu& operator=(const Stu& s){if(this != s){cout << "重载= 被调用" << endl;id = s.id;score = s.score;}return *this;}private:int id;int score;
};int main()
{Stu s1(1, 100);s1.Print();Stu s2(2, 40);s2.Print();// 调用赋值运算符重载s1 = s2;s1.Print();s2.Print();return 0;
}
注意
1.如果没有写赋值运算符的重载,系统会默认生成,如果没有申请资源,效果与上述代码中效果相同。
2.如果构造函数中申请了资源,则要注意不能直接使用默认的赋值运算符重载,负责会出现与拷贝构造函数相同的情况。
3.重载’=‘运算符,不能改变原来的含义,比如说 = 原来可以连序赋值,所以该运算符必有返回值。
4.6.4 取地址运算符重载
这个运算符一般不需要重载,除非想要特殊的输出才去进行重载。
如果对象被const修饰,会不会调用 '&'的重载?
class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print() {cout << "id:" << id << " socre:" << score << endl;}Stu* operator&(){cout << "重载& 被调用" << endl;return this;}private:int id;int score;
};int main()
{const Stu s1(1, 100);s1.Print();const Stu* s2 = &s1;return 0;
}
因为const修饰的变狼是不能被修改的,防止在方法中对该const修饰的对象进行修改,所以const修饰的变量是不能调用非const修饰的方法的。
修改成这样才可以正常被调用
const Stu* operator&() const
{cout << "重载& 被调用" << endl;return this;
}
4.6.5 其他运算符的重载示例
由于代码片段过长,在此贴一个连接,想了解的兄弟们,可以点进去看看。
点这里: gitee
5. const成员
5.1 const修饰成员变量
const修饰的成员变量具有不可更改性。const修饰的变量是不能通过其他手段修改的。
在C语言中的const可以通过指针去简介的修改const的值,但是在c++中不可以。C语言中const是一个不能修改的变量,而C++中const是一个常量。
// C++中
const int a = 10;
int arr[a]; // ok
const int a = 10;
int* pa = (int*)&a;
*pa = 100;cout << a << endl; // 输出结果为 10
c++中的const修饰的常量具有宏替换的属性,在编译期间就已经确定了const所修饰的值。
5.2 const修饰成员函数
5.2.1 概念及使用
概念
如果const修饰成员函数,将该成员函数称为const成员函数,const不能修饰普通函数指针修饰成员函数。
被const修饰的成员函数,实际是修饰该成员函数的隐含 this
指针,表明该函数在成员函数中不能对类的成员做任何修改。
写法
const int Test(const int a) const
第一个const 修饰函数的返回值
第二个const 修饰函数的参数,在函数体内不能改变形参
第三个const 修饰函数本身:
本质是在修饰隐含的this指针const Test* const
在这个函数中,this所指向的成员变量不能修改,可以读取成员中的内容。
注意
如果想在const成员函数中修改某个成员变量,在变量声明前加上 mutable
关键字,可以在const成员函数中修改该变量。
class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print() const{id = 10;cout << "id:" << id << " socre:" << score << endl;}private:mutable int id;int score;
};
5.2.2 对取地址运算符的重新重载
使用const修饰
class Stu
{
public:// 构造函数Stu(int _id, int _score){id = _id;score = _score;}void Print() const{id = 10;cout << "id:" << id << " socre:" << score << endl;}const Stu* operator&() const{cout << "重载& 被调用" << endl;return this;}private:mutable int id;int score;
};int main()
{const Stu s1(1, 100);s1.Print();const Stu* s2 = &s1;return 0;
}
6. static成员
6.1 概念与初始化
声明为static
的类成员称为类的静态成员
,用static
修饰的成员变量,称为静态成员变量
,用static
修饰的成员函数,称为静态成员函数
。
例如,要统计这个类中共创建了多少个对象?
class Test
{
public:Test(){++count;}Test(const Test& t){++count;}~Test(){--count;}// 定义成员函数static int getCount(){a = 10; // err 不能访问非静态成员return count;}private:// 定义静态成员变量 所有成员共享static int count;int a;
};// 初始化静态成员变量
int Test::count = 0;int main()
{Test t1;Test t2(t1);t2 = t1;// 通过如下三种方式都可以正常的访问静态成员函数,说明静态成员函数与具体的某个对象无关。// 是所有对象共享的函数cout << t1.getCount() << endl;cout << t2.getCount() << endl;cout << Test::getCount() << endl;return 0;
}
6.2 修饰成员变量与成员函数的特性
static
不能修饰构造、拷贝构造、赋值运算重载、析构、虚函数。
6.2.1 修饰成员变量
- 静态成员变量只有一份,所有对象共享一个静态成员变量。
- 外部成员不能访问静态成员变量
- 静态成员变量的初始化必须放在类外进行初始化,不能再构造函数的初始化列表位置对静态成员变量来初始化。
- static成员变量不会存储在对象中,因此使用
sizeof
不会影响结果- 访问方式:
类名::静态成员变量名
或对象名.静态成员变量名
6.2.1 修饰成员函数
- 静态成员函数没有隐藏的
this
指针,不能访问对象中的任何非静态成员
静态成员函数,没有this指针,不能访问非静态的成员变量,只能够访问静态成员变量,不能调用非静态的成员函数,因为非静态成员函数又的参数列表中有一个隐含的this
指针,因为静态成员函数中没有包含隐藏的this
指针,所以无法调用含有this
的成员函数。- 静态成员函数和对象无关,也可以不通过对象名进行调用
对象.静态成员函数
或类名::静态成员函数
7. 友元
通过友元声明的类和函数,可以直接访问类中的私有成员,同时,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部进行声明,声明时要加上friend
关键字。
7.1 友元函数
7.1.1 友元函数的引入
1. 问题的提出
自定义了一个类型,如果想利用内置关键字cout
对该自定义对象输出,就需要对<<
运算符进行重载,以适应对自定对象的输出。
那么会有如下三种重载方式:
1.1 直接在类中对该操作符重载
class Date
{
public:Date(int _year = 2021, int _month = 6, int _day = 1): year(_year), month(_month), day(_day){}// 重载 << 操作符ostream& operator<<(ostream& _cout){_cout << year << "-" << month << "-" << day;return _cout;}private:int year;int month;int day;
};int main()
{Date d(2021, 6, 15);// 调用重载函数d.operator<<(cout) << endl;;d << cout << endl;return 0;
}
这种方式进行重载,虽然可以实现直接输出对象的内容,但是调用方式,可以看到,非常不友好。
因为成员函数有一个隐含的this
指针,作为参数列表的第一个参数,所以this指针默认时第一个参数也就是左操作数,所以只能通过上述方式进行调用。
1.2 重载为全局函数
class Date
{
public:Date(int _year = 2021, int _month = 6, int _day = 1): year(_year), month(_month), day(_day){}int GetYear(){return year;}int GetMonth(){return month;}int GetDay(){return day;}private:int year;int month;int day;
};// 重载 << 操作符
ostream& operator<<(ostream& _cout, Date d)
{// 无法直接访问对象中的私有成员,//_cout << d.year << "-" << d.month << "-" << d.day;// 1. 可以将私有成员改为公有成员,但会影响程序整体的封装性(不用这个)// 2. 可以在成员中加入公有的方法用来进行私有成员的访问_cout << d.GetYear() << "-" << d.GetMonth() << "-" << d.GetDay();return _cout;
}int main()
{Date d(2021, 6, 15);// 调用重载函数cout << d << endl;return 0;
}
上述代码中,可以重载
<<
为全局函数,可以方便调用,但是全局函数,无法访问类的内部成员,只能通过给定方法进行内部成员的访问。并且增添了函数调用,开销大,效率低。较为麻烦,所以有了友元这个概念。
1.3 通过友元函数进行重载
class Date
{
public:Date(int _year = 2021, int _month = 6, int _day = 1): year(_year), month(_month), day(_day){}// 对友元函数的声明friend ostream& operator<<(ostream& _cout, Date d);private:int year;int month;int day;
};// 重载 << 操作符
ostream& operator<<(ostream& _cout, Date d)
{// 可以直接访问对象中的私有成员,_cout << d.year << "-" << d.month << "-" << d.day;return _cout;
}int main()
{Date d(2021, 6, 15);// 调用重载函数cout << d << endl;return 0;
}
可以直接访问类中的私有成员,但它不是类中的成员函数。
7.1.2 友元函数的特征
- 可以访问类的私有成员,但不是类的成员
- 友元函数不能使用const修饰
- 定义在类外部的普通函数,需要在类中声明,可在任意位置声明
- 一个函数可以是多个类的友元函数,可以访问多个类的私有成员
7.2 友元类
概念:可以在一个类中访问另外一个类的私有成员。
// 声明B类
class B;class A
{friend class B; // 声明B为A的友元类,在B中就可以直接访问A的私有成员public:A(int _a = 1, int _b = 2, int _c = 3): a(_a), b(_b), c(_c){}private:int a;int b;int c;
};class B
{
public:// 直接访问类A中的私有成员void setB(){d = test.a;e = test.b;f = test.c;}private:int d;int e;int f;A test;
};
友元类的特性
- 友元关系是单向的,不具有交换性
- 友元关系不能传递
- 友元关系不能继承
8. 内部类
概念:在一个类A的内部,在定义一个类B,B则称为A的内部类。
class A
{
private:static int k;int h;public:class B{public:void foo(const A& a){cout << k << endl;cout << a.h << endl;}};
};int A::k = 1;int main()
{A::B b;b.foo(A());return 0;
}
特性:
- 内部类可以定义在外部类的任何限定符之后
- 内部类可以直接访问外部类中的所有成员,相当于外部的友元
sizeof(外部类) = 外部类大小
,与内部类没有关系
总结
以上就是就是关于面向对象方面所有知识点的总结,如果发现问题,欢迎各位大佬批评指针。
这篇关于[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!