注册域名后网站建设,关键词林俊杰百度云,定制开发平台,ppt模板简洁大方文章目录 C类和对象封装类的封装性类的初识构造和析构构造和析构函数定义构造的分类以及调用深拷贝与浅拷贝初始化列表与成员对象对象成员的初始化列表 explicit 关键字动态对象的创建对象的创建new operator给基本对象申请空间给对象申请空间 静态成员静态成员变量静态成员函数… 文章目录 C类和对象封装类的封装性类的初识构造和析构构造和析构函数定义构造的分类以及调用深拷贝与浅拷贝初始化列表与成员对象对象成员的初始化列表 explicit 关键字动态对象的创建对象的创建new operator给基本对象申请空间给对象申请空间 静态成员静态成员变量静态成员函数const 静态成员 this指针使用 constconst 修饰成员函数const 修饰对象 友元普通全局函数作为友元类的成员函数作为另一个类的友元类作为另一个类的友元 运算符的重载可重载的运算符运算符与友元函数运算符与成员函数重载/--重载-和*重载赋值运算符重载 ! 、 、 () 等运算符号 继承和派生继承的访问控制代码示例 继承中的构造和析构继承中的同名变量和同名函数同名变量同名函数静态同名成员和函数 多继承菱形继承 虚继承 多态虚函数应用 虚析构纯虚函数和抽象类纯虚函数和多继承纯虚析构函数 虚函数和纯虚函数、虚析构和纯虚析构重载、重写、重定义重载重定义 - 隐藏父类函数重写 - 覆盖父类函数 C类和对象
封装
类的封装性
封装 把变量和函数合成一个整体 对变量和函数进行访问控制 类内部没有访问权限之分所有成员可以相互访问 在类的外部访问权限才有意义。public protected private 在累的外部值public修饰的成员才能被访问在没有涉及继承与派生时privateg和protected是同等级的外部不允许访问。
类的初识
class 类名 // 私有抽象概念 系统不会给其分配空间
{
public: // 公有 类的外部可访问
protected: // 保护 类的外部不可访问
private: // 私有 类的外部不可访问
};#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:void say(){money 100;cout 我是傻逼 endl;}
protected:int age;
private:int money;
};
int main(int argc, char* argv[])
{Person p;// 虽然不可访问私有数据但是可以使用公有方法访问私有变量p.say();return 0;
}stuct和class的区别是struct的默认权限是公有的而class默认权限是私有的。
将成员变量设为私有可以赋予客户端访问数据的一致性。如果成员变量不是public客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数客户在访问类成员的时候只会访问默认函数不需要考虑访问成员需不需要添加()
可以细微划分访问控制使用成员函数可以使得我们对变量的控制处理更加精细。如果我们让所有成员变量为public每个人都可以读写它。如果我们设置为private我们可以实现不准访问只读访问读写访问设置可以写出只写访问。
构造和析构
构造函数和析构函数这两个函数会被编译器自动调用构造函数完成对象的初始化动作析构函数在对象结束的时候完成清理工作。
如果你不提供构造函数和析构函数编译器会给你增加默认的操作但是默认操作不会做任何操作。
构造函数是创建对象时为成员属性赋值构造函数由编译器自动调用无需手动调用。析构函数主要用于对象销毁的时候自动调用执行一些清理操作
构造和析构函数定义
构造函数名和类名相同没有返回值但是可以有参数 ClassName(){}
析构函数在类名前加~没有返回值不能重载不能有参数 ~ClassName(){}
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:Person(){cout 无参构造函数 endl;}Person(int num){this-num num;cout 有参数构造函数 endl;}Person(const Person other){cout 拷贝构造函数 endl;}~Person(){cout 析构函数 endl;}
private:int num;
};
int main(int argc, char* argv[])
{Person p;Person p1(1);return 0;
}构造的分类以及调用
构造函数的分类
按照参数类型无参构造和有参构造
按照类型分类普通构造和拷贝构造
构造函数的调用
无参构造的调用形式
Person p; // 隐式调用
Person p1 Person(); // 显示调用
Person(); // 匿名对象调用有参构造的调用形式
Person p(1); // 隐式调用
Person p1 Person(1); // 显示调用
Person p2 20; // 隐式转换调用(只针对于有一个参数),尽量别用该方式
Person(1); // 匿名对象调用拷贝构造的调用形式默认拷贝构造是浅拷贝就对象初始化新对象的时候调用拷贝构造函数。
Person p;
Person p2 Person(p); // 显示调用
Person p3(p); // 隐式调用
Person p4 p; // 使用等号隐式转换注意 构造函数和析构函数的顺序相反。
下方不会调用拷贝构造函数
Person p(10);
Person p2;
p2 p;对于任何一个类C编译器至少会给我们写的类增加3个函数
默认构造默认析构默认拷贝构造
对类中的非静态成员属性简单的值拷贝如果用户定义了拷贝构造则C不会再提供任何默认构造函数如果用户提供了默认构造函数C不会提供默认无参构造函数但是会提供拷贝构造函数。
因此我们在设计类的时候一般需要实现无参构造有参构造拷贝构造和析构函数。
深拷贝与浅拷贝
同一个对象之间可以赋值使得2个对象的成员变量的值相同两个对象仍然是独立的两个对象这种情况被称为浅拷贝。一般情况下浅拷贝没有任何副作用但是当类中有指针并且指针指向动态分配的内存空间的时候析构函数做了动态内存释放的处理会导致内存重复释放的问题。 浅拷贝程序崩溃
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:Person(){name nullptr;num 0;cout 无参构造 endl;}Person(char *name, int num){this-name (char *) calloc(1, strlen(name) 1);if(this-name nullptr){cout 构造失败 endl;}strcpy(this-name, name);this-num num;cout 有参构造 endl;}~Person(){if(name ! nullptr){cout 空间被释放 endl;free(name);name nullptr;}cout 析构函数 endl;}void show(){cout num: num name: name endl;}
private:char *name;int num;
};
int main(int argc, char* argv[])
{Person p(aasda, 10);p.show();Person p2 p;return 0;
}深拷贝正常释放
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:Person(){name nullptr;num 0;cout 无参构造 endl;}Person(char *name, int num){this-name (char *) calloc(1, strlen(name) 1);if(this-name nullptr){cout 构造失败 endl;}strcpy(this-name, name);this-num num;cout 有参构造 endl;}Person(const Person other){this-name (char *) calloc(1, strlen(other.name) 1);strcpy(this-name, other.name);this-num other.num;}~Person(){if(name ! nullptr){cout 空间被释放 endl;free(name);name nullptr;}cout 析构函数 endl;}void show(){cout num: num name: name endl;}
private:char *name;int num;
};
int main(int argc, char* argv[])
{Person p(aasda, 10);p.show();Person p2 p;p2.show();return 0;
}初始化列表与成员对象
构造函数和其他函数不同除了有名字参数列表函数体之外还有初始化列表。
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:Person() : a(0), b(1), c(2){cout 无参构造 endl;}Person(int a, int b, int c) : a(a), b(b), c(b){cout 无参构造 endl;}void show(){cout a: a b: b c: c endl;}
private:int a;int b;int c;
};
int main(int argc, char* argv[])
{Person p;p.show();Person p2(8, 9, 10);p2.show();return 0;
}注意 初始化列表只能在构造函数中使用
对象成员的初始化列表
在类中定义数据成员一般都是基本数据类型。但是当类中的成员也可以是对象叫做对象成员。C中对对象成员的初始化是非常重要的操作当创建了一个对象的时候C编译器必须确保调用了所有子对象的构造函数。如果所有子对象有默认构造函数编译器可以自动调用他们。但是如果子对象没有默认构造函数或者想指定调用某个构造函数怎么办初始化列表提供了对象成员的构造函数调用方式。
#include iostream
#include string
#include iostream
using namespace std;
class Aaa
{
public:Aaa(int a) : aaa(a) {cout 有参构造 endl;}Aaa() {cout 无参构造 endl;}int getA(){ return aaa; }
private:int aaa;
};
class Person
{
public:Person() : a(Aaa(5)) {}Person(int a) : a(Aaa(a)) {}void show(){cout a: a.getA() endl;}
private:Aaa a;
};
int main(int argc, char* argv[])
{Person p;p.show();Person p2(8);p2.show();return 0;
}explicit 关键字
C提供了关键字explicit表示禁止通过构造函数进行隐式转换。声明为explicit的构造函数不能在隐式转换中使用。
explicit是针对只有一个参数的构造函数或者是除了第一个参数其他的都是默认值的多参数的构造函数。
动态对象的创建
在我们创建数组的时候总是需要提前预定数组长度然后编译器分配预定长度的数组空间在使用数组时会有这样的问题数组也许空间太大也许空间太小。所以对于数组而言如果能动态的分配大小空间最好不过了多以C提供了动态分配内存函数malloc和free可以在运行时候从堆中分配存储单元。然而这些函数在C中不能很好的运行因为他们不能帮我们完成对象的创建。
对象的创建
当创建一个C对象的时候会发生两件事
为对象分配内存空间。调用构造函数初始化内存。
如果我们使用C函数的话会有以下问题
程序员必须确定对象长度malloc返回的是一个void指针C不允许将void指针赋值给其他指针必须强转malloc可能申请失败必须判断返回值来保证内存分配成功用户在使用对象之前必须对它初始化构造函数不能显示调用初始化用户有可能会忘记调用初始化函数
总之C的动态内存分配函数太复杂于是C中出现了new和delete来创建和销毁一个对象。
new operator
C中解决动态分配方案就是把创建一个对象所需要的操作都结合在一个称为new的运算符里面当使用new创建一个对象时它就在堆里面为对象分配内存并调用构造函数完成初始化。
给基本对象申请空间
int main(int argc, char* argv[])
{int *p new int(100);cout *p endl;int *p2 new int[5]{1,2,3,4,5};cout p2[0] p2[1] endl;delete p;delete [] p2;return 0;
}给对象申请空间
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:Person() : a(5) {cout 构造函数 endl;}~Person(){cout 析构函数 endl;}explicit Person(int a) : a(a) {cout 有参构造函数 endl;}void show(){cout a: a endl;}
private:int a;
};
int main(int argc, char* argv[])
{// 申请空间构造函数Person *p new Person;// 析构函数销毁空间delete p;Person *p2 new Person[5];delete [] p2;Person *p3 new Person[5]{Person(3),Person(2),Person(2),Person(2),Person(2)};delete [] p3;Person *p4 new Person;void *p5 p4;delete p5; // 不会析构return 0;
}注意 如果new没有加[] 释放的时候不用加[]如果new加了[]则delete也需要加[]。
delete 无法从void中寻找析构函数释放的时候注意类型。
静态成员
在类定义中成员包括成员变量和成员函数这些成员可以使用static关键字声明为静态的称为静态成员。不管这个类创建了多少个对象静态成员只有一份拷贝这个拷贝属于这个类的对象共享。
静态成员变量
在一个类中如果将一个变量声明为static这种成员称为静态成员变量。与一个的数据成员不同无论建立多少个对象该变量只有一份静态数据的拷贝。静态成员变量属于某个类所有的对象共享。静态变量是在编译期间就分配空间对象还没创建的时候就已经分配了空间。
静态成员变量必须在类中声明在类外定义。静态数据成员不属于某个对象在为对象分配空间的时候不包括静态数据成员所占的控件。静态数据成员可以通过类名或者对象名来引用。
静态成员函数
针对于无法直接访问的private静态成员可以使用静态成员函数访问。
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:Person() : a(5) {cout 构造函数 endl;}~Person(){cout 析构函数 endl;}explicit Person(int a) : a(a) {cout 有参构造函数 endl;}void show(){cout a: a endl;}static int getB(){return b;}
private:int a;static int b;
};
int Person::b 20;
int main(int argc, char* argv[])
{cout sizeof(Person) endl;cout Person::getB() endl;return 0;
}const 静态成员
对于一个类的成员既要实现共享又要实现不可变那就用const修饰。定义静态const成员时最好在类内部初始化。
this指针
C封装性质将数据和方法封装在一起
数据和方法是分开存储的。每个对象拥有独立的数据但是对象方法是共享的。每个方法调用的时候都会隐式存在一个this指针。
C规定
this指针是隐含在成员函数内的一种指针当一个对象被创建后他的每一个成员函数都含有一个系统自动生成的隐含指针this用来保存这个对象的地址。成员函数通过this指针即可知道操作的数据对象是谁隐藏在每一个非静态成员函数中静态成员函数内部没有this指针静态成员函数不能操作非静态成员变量
使用 当形参和成员变量同名的时候可以使用this来区分。 在类的普通成员函数中返回对象本身可以使用 return *this;
const
const 修饰成员函数
使用const修饰成员函数时候const修饰this指针指向的区域成员函数体内不可以修改奔类中对的任何普通成员变量放成员变量类型钱用mutable修饰时候例外。
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:int getA() const {a 50;return a;}
private:mutable int a{0};
};
int main(int argc, char* argv[])
{Person p;cout p.getA() endl;return 0;
}函数如果不会修改成员函数的数据就给函数加const。表示在此函数内不会给成员函数赋值。
const 修饰对象
const修饰对象叫常对象编译器认为普通函数都有修改成员变量的可能。
因此不能调用非const修饰的函数。
#include iostream
#include string
#include iostream
using namespace std;
class Person
{
public:void setA(int a) const{// this-a a;}int getA() const {a 50;return a;}
private:mutable int a{0};
};
int main(int argc, char* argv[])
{const Person p;cout p.getA() endl;return 0;
}友元
类的主要特点之一是数据隐藏即类的私有成员无法在类的外部访问。但是有时候需要在类的外部访问类的私有成员怎么办解决方法就是使用友元函数友元函数是一种特权函数C允许这个特权函数访问私有成员。程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
普通全局函数作为友元
#include iostream
#include string
#include iostream
using namespace std;
class Person
{friend int getA(const Person p);
public:
private:int a{0};
};
int getA(const Person p)
{return p.a;
}
int main(int argc, char* argv[])
{const Person p;cout getA(p) endl;return 0;
}类的成员函数作为另一个类的友元
#include iostream
#include string
#include iostream
using namespace std;
class Person;
class Dog
{
public:int getA(const Person p);
};
class Person
{friend int Dog::getA(const Person p);
public:
private:int a{0};
};
int Dog::getA(const Person p)
{return p.a;
}
int main(int argc, char* argv[])
{const Person p;Dog d;cout d.getA(p) endl;return 0;
}类作为另一个类的友元
#include iostream
#include string
#include iostream
using namespace std;
class Person;
class Dog
{
public:sint getA(const Person p);
};
class Person
{friend class Dog;
public:
private:int a{0};
};
int Dog::getA(const Person p)
{return p.a;
}
int main(int argc, char* argv[])
{const Person p;Dog d;cout d.getA(p) endl;return 0;
}运算符的重载
运算符重载就是对已有的运算符重新进行定义赋予其另外一种功能以使用不同的数据类型。
运算符重载只是一种语法上的方便也就是它只是另一种函数调用。
语法函数名是由关键字operator紧跟着运算符
比如重载 : operator
注意重载运算符不要更改运算符的本质。
可重载的运算符
几乎C中的所有运算符都可以重载但是运算符重载的使用是相当受限制的。特别是不能使用C中当前没有意义的运算符不能改变运算符的优先级不能改变运算符的参数个数这样的限制有意义否则所有的这些行为产生的运算符只会混淆而不是澄清寓意。 - * / % ^ | ~ ! - * / %
^ | ! || -- -* - [] () new delete new[] delete[]不能重载的运算符有 . :: .* ?: sizeof
运算符与友元函数
#include iostream
#include string
#include iostream
using namespace std;
class Person
{friend ostream operator(ostream out, const Person p);friend Person operator(const Person p1, const Person p2);
public:
private:int a{5};
};
ostream operator(ostream out, const Person p)
{return out p.a;
}
Person operator(const Person p1, const Person p2)
{Person p;p.a p1.a p2.a;return p;
}
int main(int argc, char* argv[])
{Person p;Person p2;cout p endl;cout p2 p endl;return 0;
}运算符与成员函数
#include iostream
#include string
#include iostream
using namespace std;
class Person
{friend ostream operator(ostream out, const Person p);
public:Person operator(const Person p2){Person p;p.a this-a p2.a;return p;}private:int a{5};
};
ostream operator(ostream out, const Person p)
{return out p.a;
}
int main(int argc, char* argv[])
{Person p;Person p2;cout p endl;cout p2 p endl;return 0;
}重载/–
重载和–运算符 需要区分是前置/–还是后置/–区分方式是按照参数个数区分。
#include iostream
#include string
#include iostream
using namespace std;
class Person
{friend ostream operator(ostream out, const Person p);
public:Person operator(const Person p2){Person p;p.a this-a p2.a;return p;}Person operator(int){Person p *this;this-a;return p;}Person operator(){this-a;return *this;}Person operator--(int){Person p *this;this-a--;return p;}Person operator--(){--this-a;return *this;}
private:int a{5};
};
ostream operator(ostream out, const Person p)
{return out p.a;
}
int main(int argc, char* argv[])
{Person p;Person p2;cout p endl;cout p2 p endl;cout p endl;cout p endl;cout p2 endl;cout p-- endl;cout p endl;cout --p2 endl;return 0;
}重载-和*
#include iostream
#include string
#include iostream
using namespace std;
class Person
{friend ostream operator(ostream out, const Person p);
public:Person(){cout Person endl;}~Person(){cout ~Person endl;}void show(){cout aaa endl;}
private:int a{5};
};
class SmartPoint
{
public:SmartPoint(Person *p){this-p p;}~SmartPoint(){delete p;}Person* operator-(){return this-p;}Person operator*(){return *this-p;}private:Person *p;
};
ostream operator(ostream out, const Person p)
{return out p.a;
}
int main(int argc, char* argv[])
{SmartPoint point(new Person);point-show();(*point).show();return 0;
}注意 类中无指针成员的时候无需重写运算符斗则必须重载运算符
重载赋值运算符
指针作为类中的成员
拷贝构造函数必须自定义(默认拷贝构造是浅拷贝)必须重载运算符(默认运算符是浅拷贝)
如果不重写拷贝并且也不重载运算符编译器会给自动创建这些函数默认的行为类似于浅拷贝。
#include iostream
#include string
#include iostream
#include algorithm
using namespace std;
class Person
{friend ostream operator(ostream out, const Person p);
public:Person(){cout Person endl;}~Person(){cout ~Person endl;}
private:int a{5};
};
class SmartPoint
{
public:SmartPoint(){}SmartPoint(Person *p){this-p p;}~SmartPoint(){delete p;}explicit SmartPoint(const SmartPoint other){p new Person(*other.p);}SmartPoint operator(const SmartPoint other){cout 赋值操作 endl;p new Person(*other.p);return *this;}Person *p;
};
ostream operator(ostream out, const Person p)
{return out p.a;
}
int main(int argc, char* argv[])
{SmartPoint point(new Person);SmartPoint point2(point);SmartPoint point3;point3 point;cout *(point.p) endl;cout *(point2.p) endl;cout *(point3.p) endl;return 0;
}重载 ! 、 、 () 等运算符号
#include iostream
#include string
#include iostream
#include algorithm
using namespace std;
class Person
{friend ostream operator(ostream out, const Person p);
public:Person(int a) : a(a) {}bool operator(const Person other){return this-a other.a;}bool operator!(const Person other){return this-a ! other.a;}Person operator()(int a){this-a a;return *this;}
private:int a{5};
};
ostream operator(ostream out, const Person p)
{return out p.a;
}
int main(int argc, char* argv[])
{Person p(5);Person p2(6);Person p3(5);cout (pp2) endl;cout (p!p3) endl;Person p6(2);cout p6(5) endl;cout Person(9)(5) endl;return 0;
}注意 不要重载 || 因为用户无法实现 和 || 具有短路特性
总结 ,[],(),-操作符号只能通过成员函数进行重载 和 只能通过全局函数配合友元函数进行重载不要重载和||操作符号
运算符使用建议所有一元运算符成员 () [] - -*必须是成员 - / * ^ ! % 成员其他二元运算符非成员
继承和派生
C最终要的特性是代码重用通过继承机制可以利用已有的数据类型来定义新的数据类型新的类型不仅拥有旧类的成员还拥有新定义的成员。一个B类继承自A类或者称为从A类派生出B类。这样的话A类称为基类(父类)B类称为派生类(子类)派生类中的成员包含两大部分一类是从基类继承过来的一类是自己增加的成员从基类继承过来的表现其共性而新增的成员体现了其个性。
子类继承于父类
父类派生出子类
继承的访问控制
代码示例
class Derive : 继承方式 Base [, 继承方式 Base2 ... ]
{
// ......
}继承方式分类
访问权限
public公有继承protected保护继承private私有继承
父类个数
单继承指每个派生类只继承了一个基类的特征。多继承指多个基类派生出一个派生类的继承关系多继承的派生类直接继承了不止一个基类的特性。
注意 子类继承父类子类拥有父类中全部成员变量和成员方法(除了构造和析构之外的成员方法)但是在派生类中继承的成员并不一定能直接访问不同的继承方式会导致不同的访问权限。
代码示例
public继承在类外可以访问父类的public变量在类内可以访问public和protected数据。私有数据在子类内外都是不可见的。
#include iostream
#include string
#include iostream
#include algorithm
using namespace std;
class Base
{
public:int a{1};
protected:int b{2};
private:int c{3};
};
class Derive : public Base
{
public:void show(){cout a b endl;}
};
int main(int argc, char* argv[])
{Derive d;cout d.a endl;return 0;
}protected继承在类内可以访问public和protected数据。类外无法访问private是不可见数据。
#include iostream
#include string
#include iostream
#include algorithm
using namespace std;
class Base
{
public:int a{1};
protected:int b{2};
private:int c{3};
};
class Derive : protected Base
{
public:void show(){cout a b endl;}
};
int main(int argc, char* argv[])
{Derive d;return 0;
}private继承在类内可以访问public和protected数据。类外无法访问private是不可见数据。
#include iostream
#include string
#include iostream
#include algorithm
using namespace std;
class Base
{
public:int a{1};
protected:int b{2};
private:int c{3};
};
class Derive : private Base
{
public:void show(){cout a b endl;}
};
int main(int argc, char* argv[])
{Derive d;d.show();return 0;
}继承中的构造和析构
子类是由父类成员叠加子类新成员而成。
构造顺序是 先父类再子类析构顺序相反如果有对象成员则是先父类再对象成员再子类析构顺序相反。
#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Other
{
public:Other(){cout Other endl;}~Other(){cout ~Other endl;}
};
class Base
{
public:Base(){cout Base endl;}~Base(){cout ~Base endl;}
};
class Derive : private Base
{
public:Derive(){cout Derive endl;}~Derive(){cout ~Derive endl;}Other o;
};
int main(int argc, char* argv[])
{Derive d;return 0;
}继承中的同名变量和同名函数
同名变量
当父类和子类中成员变量同名时默认会选择子类成员如果想访问父类同名成员则必须加上父类的作用域
#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Base
{
public:int num{10};
};
class Derive : public Base
{
public:int num{20};
};
int main(int argc, char* argv[])
{Derive d;cout d.num endl;cout d.Base::num endl;return 0;
}同名函数
当子类实现父类同名成员函数时候父类的所有同名函数将会被子类屏蔽。如果用户必须调用父类的同名函数则加上作用域即可。
#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Base
{
public:int getNum() { return num; }int getNum(int a) { return num; }private:int num{10};
};
class Derive : public Base
{
public:int getNum() { return num; }private:int num{20};
};
int main(int argc, char* argv[])
{Derive d;cout d.getNum() endl;cout d.Base::getNum() endl;cout d.Base::getNum(1) endl;return 0;
}C中基类不是所有的函数都能被继承到派生类中构造函数和析构函数用来处理对象的创建和析构操作构造和析构函数只知道对他们的特定层次的对象做什么也就是说构造函数和析构函数不能被继承必须为每一个特定的派生类分别创建另外operator也不能被继承因为他完成类似于拷贝构造的行为也就是说尽管我们知道基类如何由右边的对象初始化左边的对象的所以后成员但是这个并不意味着这对其派生类依然有效。在继承过程中如果没有重写这些函数则编译器会给这些函数生成默认函数。
静态同名成员和函数
静态成员函数和非静态成员函数的共同点
都可以被继承到派生类中如果重新定义一个静态成员函数则所有基类的其他重载函数都会被屏蔽如果我们改变基类中的一个函数的特征所以使用还函数名的基类模板都会被隐藏
#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Base
{
public:static int getNum() { return num; }static int getNum(int a) { return num; }private:static int num;
};
int Base::num{10};
class Derive : public Base
{
public:static int getNum() { return num; }private:static int num;
};
int Derive::num{20};
int main(int argc, char* argv[])
{Derive d;cout d.getNum() endl;cout d.Base::getNum() endl;cout d.Base::getNum(1) endl;return 0;
}多继承
在C中我们可以从一个类中继承也可以同时从多个类中继承这就是多继承。但是由于多继承是非常受争议的从多个类继承可能会因为函数、变量等同名导致较多的歧义。
多继承会有二义性问题如果两个基类中有同名的函数或者是变量那么通过派生类对象去访问这个函数或者是变量的时候就不能明确到底调用那个版本的函数
#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Base
{
public:int a{10};
};
class Base2
{
public:int a{20};
};
class Derive : public Base, public Base2
{
public:
};
int main(int argc, char* argv[])
{Derive d;cout d.Base::a endl;cout d.Base2::a endl;return 0;
}菱形继承
两个派生类继承同一个基类而又有某个类同时继承这两个派生类这种继承称为菱形继承或者钻石型继承。
因为继承的数据存在两份因此调用数据的时候会存在二义性问题。这可以加作用域访问对应的父类的a可以解决二义性但是继承了两份数据。
对于菱形继承带来的问题C提供了一种方式采用虚基类。
class Base
{
public:int a{10};
};
class Base2 : public Base
{
public:
};
class Base1 : public Base
{
public:
};
// Derive 中有两个a
class Derive : virtual public Base1, virtual public Base2
{
public:
};虚继承
在继承方式前面加virtual修饰叫做虚继承。使用虚继承在菱形继承的时候不存在菱形继承的问题。
class Derive : virtual 继承方式 Base
{
// ......
};#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Base
{
public:int a{10};
};
class Base2 : virtual public Base
{
public:
};
class Base1 : virtual public Base
{
public:
};
class Derive : public Base1, public Base2
{
public:
};
int main(int argc, char* argv[])
{Derive d;cout d.a endl;return 0;
}vbptr虚基类指针虚基类指针是指向虚基类表vbtable
vbtable虚基类表中存放的是数据的偏移量。
总结 使用虚继承产生vbptr 和 vbtable 的目的是为了保证不管多少个继承虚基类的数据只有一份。
当使用虚继承的时候虚基类是被共享的也就是在继承体系中无论被继承多少次对象内存模型中之后出现一个虚基类的字对象(这和多继承是完全不同的)。即使共享虚基类但是必须以后一个类来完成基类的初始化(因为所有的对象都必须完成初始化那怕是默认的)同时还不能够进行重复的初始化。
那么虚基类由谁来初始化呢C标准中选择在每次继承子类中必须书写初始化语句(因为每一次继承子类都会用来定义对象)但是虚基类的初始化是由最后的子类完成其他的初始化语句都不会调用。
多态
多态是面向对象程序设计中数据抽象和继承之外的第三个基本特征。多态性提供接口与具体实现之间的另一层隔离从而将what和how分离开来。多态性改善了代码的可读性和组织性同时也使创建的程序具有可扩展性项目不仅在最初创建时期可以去扩展而且当项目在需要新的功能时也能扩展。C支持编译时多态(静态多态)和运行时多态(动态多态)运算符重载和函数重载就是编译时多态而派生类和虚函数就是实现运行时多态。静态多态和动态多态的区别就是函数地址是早绑定(静态联编)和晚绑定(动态联编)。如果函数的调用在编译阶段就可以确定调用地址并且产生函数代码就是静态多态就是说地址是早绑定的。而如果函数的调用地址不能在编译期间确定而需要在运行时候才能决定就是运行时多态 属于晚绑定。
#include iostream
#include string
#include iostream
#include algorithm
#include fstream
using namespace std;
class Base
{
public:virtual void print(){cout Base endl;}
};
class Derive : public Base
{
public:void print() override{cout Derive endl;}
};
int main(int argc, char* argv[])
{Base *d new Derive();d-print();return 0;
}备注 如果不适用virtual main函数中的print调用的会是父类的方法而不是派生类的方法
C动态多态主要就是通过虚函数来实现的虚函数允许子类重新定义父类成员函数而子类重新定义父类虚函数的做法称为覆盖或者称为重写(override)。对于特定的函数进行动态绑定C要求在基类中声明这个函数的时候使用virtual关键字修饰动态绑定也就对virtual函数生效。为创建一个需要动态绑定的虚成员函数可以简单的在这个函数声明前加上virtual关键字。如果一个函数在基类中被声明为virtual关键字那么在所有派生类中它都是virtual的在派生类中virtual函数的重定义称为重写(override)。
注意 virtual关键字只能修饰成员函数和析构函数构造函数不能为虚函数。
虚函数
虚函数在类中会产生一个虚函数指针虚函数指针指向虚函数表
vfptr : 虚函数指针指向虚函数表
vftable : 虚函数表存放的是虚函数的入口地址
总结 如果类中不涉及到继承则函数指针偏移量指向自身的函数。 如果涉及到继承派生类会继承基类的虚函数指针和虚函数表编译器会将虚函数表中的函数入口地址更新为子类的函数地址。如果使用基类指针或者引用访问虚函数的时候会间接的调用子类的虚函数。
应用
一般使用基类指针或者引用作为函数参数
#include iostream
using namespace std;
class Base
{
public:virtual void print(){cout Base endl;}
};
class Derive1 : public Base
{
public:void print() override{cout Derive1 endl;}
};
class Derive2 : public Base
{
public:void print() override{cout Derive2 endl;}
};
void func(Base base)
{base.print();
}
int main(int argc, char* argv[])
{Derive1 d1;Derive2 d2;func(d2);func(d1);return 0;
}虚析构
虚析构函数是为了解决基类指针指向派生类对象并且基类的指针删除派生类的对象。
先看下面的例子
#include iostream
using namespace std;
class Base
{
public:~Base(){cout ~Base endl;}virtual void print(){cout Base endl;}
};
class Derive : public Base
{
public:~Derive(){cout ~Derive endl;}void print() override{cout Derive endl;}
};
int main(int argc, char* argv[])
{Base *b new Derive();delete b;return 0;
}我们看到子类并没有被析构。
如果我们把析构函数改成虚函数则发现都可以调用代码如下
#include iostream
using namespace std;
class Base
{
public:virtual ~Base(){cout ~Base endl;}virtual void print(){cout Base endl;}
};
class Derive : public Base
{
public:~Derive(){cout ~Derive endl;}void print() override{cout Derive endl;}
};
int main(int argc, char* argv[])
{Base *b new Derive();delete b;return 0;
}建议 如果类会被继承把析构函数写成虚函数
纯虚函数和抽象类
在设计类时候尝尝希望基类仅仅作为其派生类的一个接口。也就是说仅想对基类进行向上转换使用他的接口而不希望用户在实际使用的时候创建一个基类对象。同时创建一个纯虚函数允许接口中放置成员函数而不一定会要提供一段可能对这个函数毫无意义的代码。
纯虚函数格式是
virtual void function() 0; 如果一个类中有纯虚函数那么这个类就是抽象类
抽象类不能实例化对象virtual void function() 0告诉编译器在vftable中为函数保留一个位置但是这个特定的位置不放地址。
建立公共接口的目的是为了将公共的操作抽象出来可以通过一个公共接口来操作一组类且这个公共接口不需要实现或者是不需要完全实现。可以创建一个公共的类。
#include iostream
using namespace std;
class Base
{
public:virtual ~Base() default;virtual void print1() 0;virtual void print2() 0;virtual void print3() 0;virtual void print(){print1();print2();print3();}
};
class Derive1 : public Base
{
public:void print1() override{cout print1 ;}void print2() override{cout print2 ;}void print3() override{cout print2 Derive1 endl;}
};
class Derive2 : public Base
{
public:void print1() override{cout print1 ;}void print2() override{cout print2 ;}void print3() override{cout print2 Derive2 endl;}
};
int main(int argc, char* argv[])
{Base *b1 new Derive1();Base *b2 new Derive2();b1-print();b2-print();delete b1;delete b2;return 0;
}纯虚函数和多继承
多继承带来了一些争议但是接口继承可以说是一种毫无争议的运用。绝大多数面向对象的语言都不支持多继承但是绝大数面向对象语言都支持接口的概念。C中没有接口的概念但是可以通过纯虚函数实现接口。
接口中只有函数原型定义没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明并不是功能实现子类需要根据功能说明定义功能实现。
注意 除了析构函数外其他的都要声明成纯虚函数
纯虚析构函数
纯虚析构在C中是合法的但是在使用的时候有一个额外的限制必须为纯虚析构提供一个函数体。那么问题是如果给纯虚析构提供函数体的情况下怎么还能称为纯虚析构函数呢纯虚析构函数和非纯虚析构函数之间的唯一不同之处在于纯虚析构函数使得基类是抽象类不能创建对象。
#include iostream
using namespace std;
class Base
{
public:virtual ~Base() 0;
};
Base::~Base() {}
class Derive1 : public Base
{
public:
};
int main(int argc, char* argv[])
{// Base b;Base *b1 new Derive1();return 0;
}虚函数和纯虚函数、虚析构和纯虚析构
虚函数由virtual修饰的有函数体的函数。纯虚函数virtual修饰函数尾部是0没有函数体所在类为抽象类。虚析构修饰类中的析构函数。纯虚析构析构函数后面加0必须在类外实现析构函数体。
重载、重写、重定义
重载 同一个作用域 参数个数参数顺序参数类型不同 与函数返回值没有关系 const也可以作为重载条件
int func(){}
int func(int a){}
int func(int a, int b){}重定义 - 隐藏父类函数 有继承 子类重新定义父类的同名成员(非虚函数)
class Base
{
public:void func(){}void func(int, int){}
};
class Derive : public Base
{
public:void func(){}
}重写 - 覆盖父类函数
有继承子类重写父类中的虚函数函数的返回值函数的名字函数的参数必须和基类中的虚函数一致
class Base
{
public:virtual void func(){}virtual void func(int, int){}
};
class Derive : public Base
{
public:void func() override{}void func(int, int) override{}
}