过程性编程和面向对象编程的区别
之前在那篇博客上看到这个比喻,觉得特别恰当,挺容易让人理解的,现在分享一下:
用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,北京叫盖饭,东北叫烩饭,广东叫碟头饭,就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。我觉得这个比喻还是比较贴切的。
蛋炒饭制作的细节,我不太会做饭,不太清楚,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。
蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。
到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。
盖浇饭的好处就是”菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是”可维护性“比较好,”饭” 和”菜”的耦合度比较低。蛋炒饭将”蛋”“饭”搅和在一起,想换”蛋”“饭”中任何一种都很困难,耦合度很高,以至于”可维护性”比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。
面向对象的特点
面向对象技术充分体现了分解、抽象、模块化、信息隐藏等思想,可有效提高软件生产率,缩短 软件开发时间,提高软件质量,是控制复杂度的有效途径。
面向对象不仅适合普通人员,也适合经理人员。降低维护开销的技术可以释放管理者的资源,将 其投入到待处理的应用中。在经理们看来,面向对象不是纯技术的,它既能给企业的组织也能给经理 的工作带来变化。
当一个企业采纳了面向对象,其组织将发生变化。类的重用需要类库和类库管理人员,每个程序 员都要加入到两个组中的一一个:- 一个是 设计和编写新类组,另一个是应用类创建新应用程序组。面向 对象不太强调编程,需求分析相对地将变得更加重要。
面向对象编程主要有代码容易修改、代码复用性高、满足用户需求3个特点。
(1)代码容易修改
面向对象编程的代码都是封装在类里面,如果类的某个属性发生变化,只需要修改类中成员函数 的实现即可,其他的程序函数不发生改变。如果类中属性变化较大,则使用继承的方法重新派生新类。(2)代码复用性高面向对象编程的类都是具有特定功能的封装,需要使用类中特定的功能,只需要声明该类并调用 其成员函数即可。如果需要的功能在不同类,还可以进行多重继承,将不同类的成员封装到一一个类中。 功能的实现可以像积木样随意组合,大大提高了代码的复用性。
(3)满足用户需求
由于面向对象编程的代码复用性高,用户的要求发生变化时,只需要修改发生变化的类。如果用 户的要求变化较大时,就对类进行重新组装,将变化大的类重新开发,功能没有发生变化的类可以直 接拿来使用。面向对象编程可以及时地响应用户需求的变化。
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
什么是类
面向对象编程(OOP)是一种特殊的、设计程序的概念性方法,C++通过一些特性改进了C语言,使 得应用这种方法更容易。下面是最重要的OOP特性:
抽象;
封装和数据隐藏;
多态;
继承;
代码的可重用性。
为了实现这些特性并将它们组合在一起,C++所做的最重要的改进是提供了类。本篇介绍类, 将解释抽象、封装、数据隐藏,并演示类是如何实现这些特性的。本章还将讨论如何定义类、如何为 类提供公有部分和私有部分以及如何创建使用类数据的成员函数。另外,还将介绍构造函数和析构函 数,它们是特殊的成员函数,用于创建和删除属于当前类的对象。最后介绍this指针,对于有些类编 程而言,它是至关重要的。后面还将把讨论扩展到运算符重载(另一种多态)和继承,它们是 代码重用的基础。
类是C++中面向对象编程(OOP)的核心概念之一
类是用户定义的一种数据类型。 要定义类,需要描述它能够表示什么信息和可对数据执行哪些操作。类之于对象就像类型之于变量。也就是说,类定义描述的是数据格式及其用法,而对象则是根据数据格式规范创建的实体。换句话说,如果说类就好比所有著名演员,则对象就好比某个著名的演员,如蛙人Kermit。我们来扩展这种类比,表示演员的类中包括该类可执行的操作的定义,如念某一角色的台词, 表达悲伤、威胁恫吓,接受奖励等。如果了解其他OOP术语,就知道C++类对应于某些语言中的对象类型,而C++对象对应于对象实例或实例变量。
下面更具体一一些。前文讲述过下面的变量声明:
int carrots;
上面的代码将创建一个类型为int 的变量(carrots)。 也就是说,carrots 可以存储整数,可以按特定的 方式使用例如,用于加和减。
注意:类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
知道类是用户定义的类型,但作为用户,并没有设计ostram和itrem类。就像函数可以来自兩 数库一样,类也可以来自类库。ostream 和istreaem 类就属于这种情况。从技术上说,它们没有被内置 到C++语言中,而是语言标准指定的类。这些类定义位于iostraem文件中,没有被内置到编译器中。如 果愿意,程序员甚至可以修改这些类定义,虽然这不是一个好主意 (准确地说,这个主意很糟)。iostream 系列类和相关的fstream 或文件I/O)系列类是早期所有的实现都自带的唯一两组类定义.然而,ANSI/ISO C++委员会在C++标准中添加了其他些类库。 另外,多数实现都在软件包中提供了其他类定义。事实上,C++当前之所以如此有吸引力,很大程度上是由于存在大量支持UNIX、Macintosh和Windows编程的类库。
类的声明与定义
//类可以看做是结构体的升级版。
类的声明格式如下:
class 类名
{
public:
公有的数据成员和成员函数
protected:
保护的数据成员和成员函数
private:
私有的数据成员和成员函数
};
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
构造函数和析构函数
构造函数和析构函数是类体定义中比较特殊的两个成员函数,因为它们两个都没有返回值,而且
构造函数名标识符和类名标识符相同,析构函数名标识符就是在类名标识符前面加“~”符号。
构造函数主要是用来在对象创建时,给对象中的一些数据成员赋值,主要目的就是来初始化对象。
析构函数的功能是用来释放一个对象的,在对象删除前,用它来做一些清理工作,它与构造函数的功能正好相反。
类中的成员函数
类中如果有成员函数,则声明是必须的,而定义是可选的,什么意思呢,请看下例:
在类内部定义函数体
class 类名
{
返回类型 函数名(形参列表)
{
函数体
}
};
在类外部定义函数体
class 类名
{
返回类型 函数名(形参列表);
};
返回类型 类名 :: 函数名(形参列表)
{
函数体
}
看到这里会产生一个问题,那就是这两种定义方法到底有什么区别,或者根本没有区别。
其实它们还是有区别的,类内部定义的函数,程序在要调用它的时候会把它当作是一个内联函数,内联函数的好处是调用速度更快,但是会占用额外的内存空间,每调用一次都相当于定义一次。而外部定义的函数,就不会被当作内联函数。对于一些要用到递归的函数,定义成内联函数肯定是不合理的。因此建议使用第二种方法定义成员函数。
类的定义一般放在程序文件开头,或者放到头文件中被程序文件包含,当然也可以放在局部作用域里。这里有必要提一下,c++规定,在局部作用域中声明的类,成员函数必须是函数定义形式,不能是原型声明。
类相当于一种新的数据类型,数据类型不占用存储空间,用类型定义一个实体的时候,才会为它分配存储空间。
类的成员函数和普通函数一样,也可以进行重载,设置默认参数,显式的指定为内联函数等。
注意:C++中对于特定的某个函数,设置默认形参这个动作只能有一次,只能在声明或定义时设置默认参数
请看下例:
class AA
{
void Sum(int a=0,int b=0);
};
void AA::Sum(int a=0,int b=0)
{
...
}
这是一个设置了默认参数的类成员函数,但这是错误的,下面这样则是正确的:
class AA
{
void Sum(int a=0,int b=0);
};
void AA::Sum(int a,int b)
{
...
}
下面的代码将详细讲解上述内容:
#include<iostream>
#include<cstring>
using namespace std;
class Student //定义学生类
{
private: //定义私有成员变量
char *Id; //学号
char *Name; //姓名
int Age; //年龄
public: //定义公有接口
//为了方便读者了解构造函数与析构函数的执行顺序和次数,我在构造函数和析构函数每一次执行完毕之后输出一行提示信息
Student() //构造函数:在对象创建时自动调用,如果没有定义编译器会自动补一个没有用的构造函数,作用:初始化对象
{
//分配内存
Id = new char[20];
Name = new char[20];
//添加内容
strcpy(Id, "无");
strcpy(Name, "无");
Age = 10;
cout << "构造函数执行完毕!" << endl; //构造函数执行完毕后的提示信息
}
//提供默认参数
void Set(char* In = (char*)"00000000", char* name = (char*)"学生", int age = 0); //设置学生信息
void Show(); //显示学生信息
~Student() //析构函数的功能是用来释放一个对象的,在对象删除前,它将被自动调用来做一些清理工作,它与构造函数的功能正好相反。
{
delete[]Id; //释放掉Id和Nmae所占的内存
delete[]Name;
cout << "构造函数执行完毕!" << endl; //析构函数执行完毕后的提示信息
}
};
//注意在类外定义时函数名前需要加 “ Student:: ” 表示这个Set()函数是类Student的成员函数
void Student::Set(char* id, char* name, int age) //设置学生学号
{
strcpy(Id, id);
strcpy(Name, name);
Age = age;
}
void Student::Show() //显示学生信息
{
cout << "学生编号为:" << Id << " " << "姓名为:" << Name << " " << "年龄为:" << Age << endl << endl;
}
int main(void)
{
//创建两个Student对象
Student A, B;
cout << "A的输出为:" << endl;
A.Show(); //A使用构造函数提供的内容
cout << "B的输出为:" << endl;
B.Set((char*)"20200910", (char*)"李小明", 66); //B使用用户提供的内容
B.Show();
return 0;
}
C++ 类中八个默认的函数
1、默认构造函数;
2、默认析构函数;
3、默认拷贝构造函数;
4、默认重载赋值运算符函数;
5、默认重载取址运算符函数;
6、默认重载取址运算符const函数;
7、默认移动构造函数(C++11);
8、默认重载移动赋值操作符函数(C++11)。
class A
{
public:
// 默认构造函数;
A();
// 默认拷贝构造函数
A(const A&);
// 默认析构函数
~A();
// 默认重载赋值运算符函数
A& operator = (const A&);
// 默认重载取址运算符函数
A* operator & ();
// 默认重载取址运算符const函数
const A* operator & () const;
// 默认移动构造函数
A(A&&);
// 默认重载移动赋值操作符
A& operator = (const A&&);
};
前两个前面已经讲过了,现在讲后面六个
拷贝构造函数(Copy Constructor)
1.拷贝构造函数实际上是构造函数的重载,具有一般构造函数的所有特性,用此类已有的对象创建一个新的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。用类的一个已知的对象去初始化该类的另一个对象时,会自动调用对象的拷贝构造函数;
2.函数名与类名相同,第一个参数是对某个同类对象的引用,且没有其他参数或其他参数都有默认值,返回值是类对象的引用,通过返回引用值可以实现连续构造,即类似A(B©)这样;
3.如果没有显式定义,编译器会自动生成一个默认的拷贝构造函数,默认的拷贝构造函数会依次拷贝类的数据成员完成初始化;
4.浅拷贝和深拷贝:编译器创建的默认拷贝构造函数只会执行"浅拷贝",也就是通过赋值完成,如果该类的数据成员中有指针成员,也只是地址的拷贝,会使得新的对象与拷贝对象该指针成员指向的地址相同,delete该指针时则会导致两次重复delete而出错,如果指针成员是new出来就是“深拷贝”。
重载赋值运算符函数(Copy Assignment operator)
1.它是两个已有对象,一个给另一个赋值的过程。当两个对象之间进行赋值时,会自动调用重载赋值运算符函数,它不同于拷贝构造函数,拷贝构造函数是用已有对象给新生成的对象赋初值的过程;
2.赋值运算符重载函数参数中const和&没有强制要求,返回值是类对象的引用,通过返回引用值可以实现连续赋值,即类似a=b=c这样,返回值类型也不是强制的,可以返回void,使用时就不能连续赋值;
3.赋值运算符重载函只能定义为类的成员函数,不能是静态成员函数,也不能是友元函数,赋值运算符重载函数不能被继承,要避免自赋值;
4.如果没有显式定义,编译器会自动生成一个默认的赋值运算符重载函数,默认的赋值运算符重载函数实现将数据成员逐一赋值的一种浅拷贝,会导致指针悬挂问题。
重载取址运算符(const)函数
1.重载取址运算符函数没有参数;
2.如果没有显式定义,编译器会自动生成默认的重载取址运算符函数,函数内部直接return this,一般使用默认即可。
移动构造函数和重载移动赋值操作符函数
1.C++11 新增move语义:源对象资源的控制权全部交给目标对象,可以将原对象移动到新对象, 用于a初始化b后,就将a析构的情况;
2.移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用;
3.临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候就可以使用移动构造。移动构造可以减少不必要的复制,带来性能上的提升。
讨论
1.构造函数为什么不能有返回值?
(1).C++语言规定构造函数没有返回值;
(2).构造函数不作为右值使用,返回值也没有用;
(3).就算有返回值,从基本语义角度来讲,也应该返回的是所构造的对象,所以没必要多此一举来指定返回类型了;
(4).假如有返回值,讨论下面代码
class A
{
public:
A():m_iTest(0) { }
A(int i):m_iTest(i) { }
private:
int m_iTest;
};
按照C++的规定,A a = A();是用默认构造函数创建一个临时对象,并用这个临时对象初始化a,此时,a.m_iTest的值应该是0。现在如果A::A()有返回值,并且返回了1(表示构造成功),则C++会用1去初始化a,即调用有参数构造函数A::A(int i),得到的a.m_iTest便会是1。于是,语义产生了歧义,使得C++原本已经非常复杂的语法,进一步混乱不堪。
构造函数的调用之所以不设返回值,是因为构造函数的特殊性决定的。当然,上面的讨论,也是基于C++语言规定,如果规定构造函数可以有返回值,上面用法也许就不一样了。是先有鸡还是先有蛋,这是一个神奇的问题。总之,现在C++语法体系是这样的,如果设计构造函数可以有返回值,可能整个C++语言更难实现了。
2.对象创建和销毁过程是怎样的?
对象创建(new)过程:
(1).通过operator new申请内存;
(2).使用placement new调用构造函数(简单类型忽略此步);
(3).返回内存指针。
new和malloc的比较:
(1).new失败时会调用new_handler处理函数,malloc不会,失败时返回NULL;
(2).new能通过placement new自动调用对象的构造函数,malloc不会;
(3).new出来的东西是带类型的,malloc是void*,需要强制转换;
(4).new是C++运算符,malloc是C标准库函数。
new的三种形态:new operator,operator new,placement new
(1).new operator:上面所说的new就是new operator,共有三个步骤组成(申请内存,调用构造函数,返回内存指针),对于申请内存步骤是通过运算符new(operator new)完成的,对于调用什么构造函数,可以由placement new决定;
(2).operator new:像普通运算符一样可以被重载,operator new会去申请内存,申请失败的时候会调用new_handler处理,这是一个循环的过程,如果new_handler不抛出异常,会一直循环申请内存,直到成功;
(3).placement new:用于定位构造函数,在指定的内存地址上用指定类型的构造函数构造对象。
对象销毁(delete)过程:
(1).调用析构函数(简单类型忽略此步);
(2).释放内存。
delete和free比较
(1).delete能自动调用对象的析构函数,free不会;
(2).delete是C++运算符,free是C标准库函数。
3.拷贝构造函数参数为什么必须使用类类型对象引用传递?
传参的位置如果一直调用拷贝构造函数,也就是会递归引用,导致栈溢出。
4.赋值运算符重载函数为什么要避免自赋值?
(1).提高效率。自赋值无意义,如果自赋值,可以立即return *this;
(2).如果不避免,当类的数据成员中如果含有指针,自赋值时会造成内存泄漏。
this指针
1.一个类中的不同对象在调用自己的成员函数时,其实它们调用的是同一段函数代码,那么成员函数如何知道要访问哪个对象的数据成员呢?
没错,就是通过this指针。每个对象都拥有一个this指针,this指针记录对象的内存地址,当我们调用成员函数时,成员函数默认第一个参数为T* const register this,大多数编译器通过ecx寄存器传递this指针,通过 this 这个隐式参数可以访问该对象的数据成员。
2.类的成员函数为什么不能用static和const同时修饰?
类中用const修饰的函数通常用来防止修改对象的数据成员,函数末尾的const是用来修饰this指针,防止在函数内对数据成员进行修改,而静态函数中是没有this指针的,无法访问到对象的数据成员,与C++ static语义冲突,所以不能。
二、this指针注意点
1.C++中this关键字是一个指向对象自己的一个常量指针,不能给this赋值;
2.只有成员函数才有this指针,友元函数不是类的成员函数,没有this指针;
3.同样静态函数也是没有this指针的,静态函数如同静态变量一样,不属于具体的哪一个对象;
4.this指针作用域在类成员函数内部,在类外也无法获取;
5.this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的。
三、this指针的使用
1.在类的非静态成员函数中返回类对象本身的时候,直接使用 return this,比如类的默认取址运算符重载函数,另外,也可以返回this的引用,这样可以像输入输出流那样进行“级联”操作;
2.修改类成员变量或参数与成员变量名相同时,如this->a = a (写成a = a编译不过);
3.在class定义时要用到类型变量自身时,因为这时候还不知道变量名,就用this这样的指针来使用变量自身。
四、this指针探讨
1.this指针是什么时候创建的?
对象new的过程中创建的,具体哪个阶段有待进一步深入了解。
- this指针存放在何处?
this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它们并不是和高级语言变量对应的。
3.为什么C++ NULL对象指针可以调用非虚成员函数,而Java中却不行?
C++语言是静态绑定的,这也是C++语言和Java语言的一个显著区别。类的成员函数并不与特定对象绑定,所有成员函数共用一份成员函数体,当程序编译后,成员函数的地址即已经确定。另外,C++只关心你的指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性。况且在有些系统上,地址0也是有效的,理论上完全可以构造一个在地址0上的对象,所以C++中nullptr对象调用成员函数并无不可 。
nullptr对象调用成员函数时,只要不访问此对象独有的内存部分,则程序正常运行,因为不会使用this,一旦访问此对象的成员变量,则程序崩溃。当然nullptr调用虚方法是不能正常运行的(虚函数有虚表,会占用内存空间),虚方法调用是依赖于this指针的。可以这样理解,你给函数传递了错误的参数,但在该函数内部并没有使用该参数,所以其不影响函数的运行。可以参考下面代码:
#include <iostream>
using namespace std;
class CPeople
{
public:
CPeople(const std::string& name, int age)
: mName(name), mAge(age){ }
~CPeople();
void Print()
{
std::cout << "show people info:" << std::endl;
}
void PrintInfo()
{
std::cout << "name:" << mName << std::endl;
std::cout << "age:" << mAge << std::endl;
}
private:
std::string mName;
int mAge;
};
int main()
{
CPeople* jon = NULL;
jon->Print(); // 程序正常运行
jon->PrintInfo(); // 程序崩溃,访问非法地址,此时mName和mAge并没有分配空间
return 0;
}
总结
引用网上关于this指针的一个经典回答:
当你进入一个房子后,
你可以看见桌子、椅子、地板等,
但是房子你是看不到全貌了。
对于一个类的实例来说,
你可以看到它的成员函数、成员变量,
但是实例本身呢?
this是一个指针,它时时刻刻指向你这个实例本身。
继承
继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一。简单的说,继承是指一个对象直接使用另一对象的属性和方法。继承呈现了 面向对象程序设 计的层次结构, 体现了 由简单到复杂的认知过程。C++中的继承关系就好比现实生活中的父子关系,继承一笔财产比白手起家要容易得多,原始类称为基类,继承类称为子类,它们是类似于父亲和儿子的关系,所以也分别叫父类和子类。继承的方式有三种分别为公有继承(public),保护继承(protect),私有继承(private)。
定义格式如下:
1.公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
2.私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
3.保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
private能够对外部和子类保密,即除了成员所在的类本身可以访问之外,别的都不能直接访问。protected能够对外部保密,但允许子类直接访问这些成员。public、private和protected对成员数据或成员函数的保护程度可以用下表来描述:
class Base //父类
{
private:
int _priB;
protected:
int _proB;
public:
int _pubB;
};
class Derived : public Base //子类,继承自base,继承类型为公有继承
{
private:
int _d_pri;
protected:
int _d_pro;
public:
void funct()
{
int d;
d = _priB; //error:基类中私有成员在派生类中是不可见的
d = _proB; //ok: 基类的保护成员在派生类中为保护成员
d = _pubB; //ok: 基类的公共成员在派生类中为公共成员
}
int _d_pub;
};
//总结:1.public继承是一个接口 继承, 保持is - a原则, 每个父类可用的成员 对子类也可用, 因为每个子类 对象也都是一个父类对象。
class C :private A //基类Base的派生类C(私有继承)
{
public:
void funct()
{
int c;
c = _priB; //error:基类中私有成员在派生类中是不可见的
c = _proB; //ok:基类的保护成员在派生类中为私有成员
c = _pubB; //ok:基类的公共成员在派生类中为私有成员
}
};
class E :protected A //基类Base的派生类E(保护继承)
{
public:
void funct()
{
int e;
e = _priB; //error:基类中私有成员在派生类中是不可见的
e = _proB; //ok:基类的保护成员在派生类中为保护成员
e = _pubB; //ok:基类的公共成员在派生类中为保护成员
}
};
int main()
{
int a;
D d;
a = D._priB; //error:公有继承基类中私有成员在派生类中是不可见的,对对象不可见
a = D._proB; //error:公有继承基类的保护成员在派生类中为保护成员,对对象不可见
a = D._pubB; //ok:公有继承基类的公共成员在派生类中为公共成员,对对象可见
B b;
a = c._priB; //error:私有继承基类中私有成员在派生类中是不可见的,对对象不可见
a = c._proB; //error:私有继承基类的保护成员在派生类中为私有成员,对对象不可见
a = c._pubB; //error:私有继承基类的公共成员在派生类中为私有成员,对对象不可见
C c;
a = e._priB; //error:保护继承基类中私有成员在派生类中是不可见的,对对象不可见
a = e._proB; //error:保护继承基类的保护成员在派生类中为保护成员,对对象不可见
a = e._pubB; //error:保护继承基类的公共成员在派生类中为保护成员,对对象不可见
return 0;
}
4.不管是哪种继承方式, 在派生类内 部都可以访问基类的公有成员 和保护成员 基类的私有成员 存在但是在子类中不可见( 不能访问)。
5.使用关键字class时默认的继承方式是private,不过最好显示的写出继承方式。
6.在实际运用中一般使用都是public继承, 极少场景下才会使用protetced/private继承。
继承关系&访问限定符
友元
概述
在C++中,我们使用类对数据进行了隐藏和封装,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
知识点
友元关系不能被继承。
友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的声明
如果将类的封装比喻成一堵墙的话,那么友元机制就像墙上了开了一个门,那些得到允许的类或函数允许通过这个门访问一般的类或者函数无法访问的私有属性和方法。友元机制使类的封装性得到消弱,所以使用时一定要慎重。
通常对于普通函数来说,要访问类的保护成员是不可能的,如果想这么做那么必须把类的成员都声明为public(共用的),然而这做带来的问题遍是任何外部函数都可以毫无约束的访问它操作它,c++利用friend修饰符,可以让一些你设定的函数能够对这些保护数据进行操作,避免把类成员全部设置成public,最大限度的保护数据成员的安全。
友元能够使得普通函数直接访问类的保护数据,避免了类成员函数的频繁调用,可以节约处理器开销,提高程序的效率,但所矛盾的是,即使是最大限度大保护,同样也破坏了类的封装特性,这即是友元的缺点,在现在cpu速度越来越快的今天我们并不推荐使用它,但它作为c++一个必要的知识点,一个完整的组成部分,我们还是需要讨论一下的。 在类里声明一个普通数学,在前面加上friend修饰,那么这个函数就成了该类的友元,可以访问该类的一切成员
友元函数
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明
友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
友元函数的调用与一般函数的调用方式和原理一致。
友元分为三种:友元函数、友元类、友元成员函数。
声明在类中加上friend,定义在类外,不可加friend、以及类名::。
下面转载的一个代码:
#ifndef A_H
#define A_H
#include <iostream>
using std::cout;
using std::endl;
class B;//前向声明
class A
{
public:
void dis(const B &b);//只是一个成员函数声明,故B无需现在创建,可以用前向声明 class B;
};
#endif
#ifndef B_H
#define B_H
//class A;//因先 #include "A.h" ,再包含 #include "B.h" ,故可以省略前向声明(A已先定义)
class B
{
private:
int i;
public:
B(int v):i(v){ }
B():i(0){ }
//友元声明
friend void A::dis(const B &a);//其它类的成员函数作为友元函数
friend void test(const B& b);//普通的非成员函数作为友元函数
};
#endif
#ifndef C_H
#define C_H
class C
{
private:
int c;
public:
C(int v):c(v){ }
friend class D;//友元类,只是一个声明,不作为成员一部分,故无需先创建D的定义
};
#endif
#ifndef D_H
#define D_H
#include <iostream>
using std::cout;
using std::endl;
//class C;//因先 #include "C.h" ,再包含 #include "D.h" ,故此语句可以省略
class D
{
public:
void dis(const C& c)//dis成员函数定义,这里要用到形参C,故C必须先定义完整,所以先#include "C.h"
{
cout<<c.c<<endl;
}
};
#endif
#include <iostream>
#include "A.h"
#include "B.h"
#include "C.h"
#include "D.h"
using namespace std;
void A::dis(const B &b)//必须最后定义,因需要两个类完整定义后,才能定义该成员函数
{
cout<<b.i<<endl;
}
void test(const B &b)//普通的非成员函数作为友元函数
{
cout<<b.i<<endl;
}
int main()
{
//其他类的成员函数作为友元函数
A a;
B b(9);
a.dis(b);
//普通的非成员函数作为友元函数
test(b);
//友元类举例
C c(10);
D d;
d.dis(c);
system("pause");
return 0;
}
说明:友元的引入,为了正确地构造类,需要注意友元声明与定义之间的相互依赖。否则会导致编译出错。以上为实例代码,可以借鉴。
虚函数
C++的特性使得我们可以使用函数继承的方法快速实现开发,而为了满足多态与泛型编程这一性质,C++允许用户使用虚函数 (virtual function) 来完成 运行时决议 这一操作,这与一般的 编译时决定 有着本质的区别。
虚函数表实现原理
虚函数的实现是由两个部分组成的,虚函数指针与虚函数表。
虚函数指针
虚函数指针 (virtual function pointer) 从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。
虚函数指针是确实存在的数据类型,在一个被实例化的对象中,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。与对象的成员不同,虚函数指针对外部是完全不可见的,除非通过直接访问地址的做法或者在DEBUG模式中,否则它是不可见的也不能被外界调用。
只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数也都会对应一个虚函数指针。所以拥有虚函数的类的所有对象都会因为虚函数产生额外的开销,并且也会在一定程度上降低程序速度。与JAVA不同,C++将是否使用虚函数这一权利交给了开发者,所以开发者应该谨慎的使用。
虚函数表:
看的我头都歪了,哈哈
请参考这位大佬的文章
上文已经提到,每个类的实例化对象都会拥有虚函数指针并且都排列在对象的地址首部。而它们也都是按照一定的顺序组织起来的,从而构成了一种表状结构,称为虚函数表 (virtual table) 。
我们先来规定一个基类
class Base
{
public:
virtual void f(){ cout<<"Base::f"<<endl;}
virtual void g(){ cout<<"Base::g"<<endl;}
virtual void h(){ cout<<"Base::h"<<endl;}
};
首先对于基类Base它的虚函数表记录的只有自己定义的虚函数
接下来我们来看看子类的情况
class Derived:public Base
{
public:
virtual void f(){ cout<<"Derived::f"<<endl;}
virtual void g1(){ cout<<"Derived::g1"<<endl;}
virtual void h1(){ cout<<"Derived::h1"<<endl;}
}
·一般覆盖继承
首先是最常见的继承,子类Derived对基类的虚函数进行覆盖继承,在这个例子中仅设计了一个函数继承的情况以此推广情况。
那么此时情况是这样的:
首先基函数的表项仍然保留,而得到正确继承的虚函数其指针将会被覆盖,而子类自己的虚函数将跟在表后。
而当多重继承的时候,表项将会增多,顺序会体现为继承的顺序,并且子函数自己的虚函数将跟在第一个表项后。
C++中一个类是公用一张虚函数表的,基类有基类的虚函数表,子类是子类的虚函数表,这极大的节省了内存
同名覆盖原则与const修饰符
如果继续深入下去的话我们将会碰见一个有趣的状况
class Base
{
public:
virtual void func()const
{
cout << "Base!" << endl;
}
};
class Derived :public Base
{
public:
virtual void func()
{
cout << "Derived!" << endl;
}
};
void show(Base& b)
{
b.func();
}
Base base;
Derived derived;
int main()
{
show(base);
show(derived);
base.func();
derived.func();
return 0;
}
在上述程序中我们将Base类中的虚函数base定义为const类型,我们知道const后缀的目的是为了限定该函数不对类内成员做出修改。然后我们分别声明base与derived并且通过show函数调用它们的func函数,子类传参给父类也是非常正常的一个操作,但是结果可能却令人不解:
Base!
Base!
Base!
Derived!
这里有一个很大的问题,因为当我们将derived传过去的时候并没有调用derived的虚函数!也就是说虚函数不再是多态的了。
但是的话我们只需要简单的修改任意一项:将line4结尾的const限定符去掉或者将Derived的func1后加上const便可以使一切正常。这是为什么呢?
很多其他的博客将其解释为是const符号作用的原因,但实际上这样的解释并不正确。正确的原因是:
虚函数的声明与定义要求非常严格,只有在子函数中的虚函数与父函数一模一样的时候(包括限定符)才会被认为是真正的虚函数,不然的话就只能是重载。这被称为虚函数定义的同名覆盖原则,意思是只有名称完全一样时才能完成虚函数的定义。
因此在上述的例子中,将Derived类型的子类传入show函数时,实际上类型转化为了Base,由于此时虚函数并未完成定义,Derived的func()此时仅仅是属于Derived自己的虚函数,所以在show中b并不能调用,而调用的是Base内的func。而当没有发生类型转换的时候,Base类型与Derived类型就会各自调用自己的func函数。
参考资料:
C++ Primer Plus 作者:Stephen Prata
C程序设计 作者:潭浩强
Essential C++ 作者:Stanley B.Lippman
参考文章链接,作者:KeepHopes
参考文章链接,作者:唐世光
参考文章链接,作者:A彡安静氵
参考文章链接,作者:haoel
本篇到此结束,有什么需要补充的欢迎在评论区留言哦~
群内有各种学习资料,欢迎大家一起来学习!
如果大家遇到什么问题也欢迎大家进群讨论~
qq群:759252814
期待你的关注~
感谢大家的支持,谢谢!