前言知识点
struct 和
class的默认权限不同,
struct默认权限
public;class 默认权限是
private;
.
和::
和:
和->
,在此整理一下这些常用符号的区别。
1、A.B则A为对象或者结构体,B为成员变量或者成员函数;
2、A->B则A为指针,->是成员提取,A->B是提取A中的成员B,A可以是指向类、结构、union的指针;
3、::是作用域运算符,A::B表示作用域A中的名称B,A可以是名字空间、类、结构;
4、:一般用来表示继承;
类
类声明:通常数据成员放在私有部分,成员函数放在公有部分。类声明通常在头文件中声明。类实现通常放在同名的cpp文件中实现;
class className
{
private:
data_member declarations;//通常成员变量名后加_与普通变量进行区分
public:
member_function prototypes;
};
公共权限: 类内类外都可以访问;
保护权限: 类(子类也)内可以访问,类外不可以;
私有权限: 类内可以访问,但是子类不可以访问,类外访问不到;
(1)this (this 指针指向被调用的成员函数所属的对象的地址)
this指针,本质是一个指针常量,即指针指向不可以修改
任何对类成员的直接访问都被看作this的隐式引用。
std::string isbn() const {
return bookNo;//成员变量
}
std::string isbn() const {
return this->bookNo;
} // 两者等价
this指针使用:
1当形参和成员变量同名时,可以使用this指针区分;
2在类的非静态成员函数中返回对象本身,可以使用return *this
,另外返回类型必须是引用,不然返回的是副本,不是本身;
(2)在类的外部定义成员函数
类外部定义的成员的名字必须包含它所属的类名。
double Sales_data::avg_price() const {//Sales_data是类名,avg_price()是成员函数名
//函数具体实现
}
构造函数
(3)构造函数 (自动调用一次)
定义:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数:
构造函数没有返回类型;void 都不能写
构造函数可以有参数,可以重载;
构造函数的名字和类名相同。
如果定义了某种构造函数,编译器将不会定义默认的构造函数,需要自己提供。
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。
构造函数的分类:
按照参数分类: 无参构造和有参构造
按照类型分类:
复制构造函数
: 接收其所属类的对象作为参数 原型:classname(const classname &name);
***当类成员含有地址或指针时,如果使用默认的复制构造函数,则会多个对象指向同一个地址,当大于1个对象释放时,会多次释放该地址,从而报错,因此需要提供显示复制构造函数。 简单判断,当类中有使用new初始化的成员,就需要显式化复制构造函数,用来复制指向的数据,而不是指针,称为深度复制。需要注意自我赋值,记得删除之前指向的内存,
如果使用到赋值运算符,在上述情况下,也会有同样的问题,需要显式构造赋值运算符 ----记得在函数内对赋值自身的情况判断;
//example 一个不是很恰当的例子,只为表明为什么要深度赋值
class student
{
private:
string name;
int age;
int *score; //数组保存多个成绩
public:
student(string s, int a,int *scor):name(s), age(a),score(scor){};
student(const student& s);//复制构造函数
student& operator=(const student & s);
void modify(int a,int b);
void print();
};
student& student::operator=(const student & s){
if(this == &s)
return *this;
this->name = s.name;
this->age = s.age;
int* sc = new int[2]{s.score[0],s.score[1]};//注意这里是深拷贝
this->score = sc;
}
void student::modify(int a, int b){
score[a] = b;
}
void student::print(){
cout<<"name = "<<name<<" ,age = "<<age<<endl;
cout<<"score :";
for(int a = 0; a<2;a++)
{
cout<<" "<<score[a];
}
cout<<endl;
}
student::student(const student &s){
this->age = s.age;
this->name = s.name;
int sc = new int[2]{s.score[0],s.score[1]};
this->score = sc;
}
//main()
int s[2] = {100,100};
student stu1("ssss",12,s);
//当没有显示复制构造函数(自己注销掉上面的显示构造函数),自己注释掉上面给出的给出的构造函数,看一下
student stu2(stu1);//会将stu1中的参数复制给stu2,对name,age数据是正常的,但对score则会使得stu1和stu2指向同一个地址,会出现以下情况,若修改stu1的成绩,stu2也会改变,同样,删除掉stu1,则存在的stu2的score数据会指向异常;
stu2.print();//100 100
stu1.modify(1,60);
stu2.print();//100 60
//显示复制构造函数,则两次打印结果相同
//重载=运算符也是一样的效果
移动构造函数:
接受右值的对象作为函数参数 原型: classname(classname && name){};
将name的元素给该对象,同时对于和地址相关的,记得将name的某些成员指向空;不然就涉及上述的释放问题。 形参一般就是右值,如c1+c2
student(student&& s):name(s.name),age(s.age)
{
this->score = s.score;
s.score = nullptr;
}
移动赋值构造函数:
上述情况下,也适合使用移动赋值构造函数
student& student::operator=(student && s){
if(this == &s)
return *this;
delete[] score;
name =s.name;
age = s.age;
score = s.score;
s.score = nullptr;
return *this
}//其中age为int 型, pc 为 char*
移动构造函数与拷贝构造函数对比
:
原型: classname(classname && name);形参为右值引用
原型:classname(const classname &name); 形参为左值引用
拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。而对于指针参数来讲,需要注意的是,移动构造函数是对传递参数进行一次浅拷贝。也就是说如果参数为指针变量,进行拷贝之后将会有两个指针指向同一地址空间,这个时候如果前一个指针对象进行了析构,则后一个指针将会变成野指针,从而引发错误。所以当变量是指针的时候,要将指针置为空,这样在调用析构函数的时候会进行判断指针是否为空,如果为空则不回收指针的地址空间,这样就不会释放掉前一个指针。
复制构造函数的调用时机:
1将新对象初始化为一个同类对象;
2值传递的方式给函数参数传值;
void dowork(Person p){...}
Person p;
dowork(p); //-------------会调用复制构造函数,在把实参按值传递给形参时;
3以值方式返回局部对象;
Person dowork(){
Person p1;
return p1;
}
Person p3 = dowork();
//局部对象也会调用复制构造函数,将p1复制给p3;p3和p1地址是不一样的;
赋值构造函数使用时机:使用等号的地方可能是。
默认的方法和禁止的方法
当我们想使用某个默认的函数,但该函数由于某些原因不会自动创建,则可以使用关键字default
显式声明
:如 classname() = default;
当我们想编译器禁止使用特定方法时,可以使用delete
关键字;方法同上;—类似于将该函数定义放在private;—将不想使用的函数放在private声明,则类外就不能使用到,如不想使用默认的无参构造,则可以把无参构造声明在private内
关键字default只能用于6个特殊成员函数
,但delete关键字可以用于任何函数。
delete可以用来禁止特定的转换;
如:类函数: void redo(double);
如果传入5,则会提升为5.0,在使用该方法,若想禁止这种转换,只需要声明: void redo(int) = delete;
声明时使用default和delete,而不是在定义的时候使用;
class A{
private:
double num;
public:
A(double a):num(a){}
A(int) = delete;//禁止int
A() = default;//默认构造函数
A& operator=(const A&) = delete; //禁止赋值构造
};
1括号法:
默认构造函数调用: Person p1;
** 调用默认构造函数时,不要加括号,因为编译器会认为是一个函数的声明***
有参构造函数: Person p2(10);
复制构造函数: Person p3(p2);
2显示法:
默认构造函数如上;
有参构造函数: Person p2 = Person(10);
复制构造函数: Person p3 = Person(p2);
****Person(10) ; 匿名对象: 当前行执行结束,系统会立即回收匿名对象
****Person(p2)不要利用复制构造函数来初始化匿名对象,编译器会认为 Person(p2) 是Person p2;对象的声明,重定义
3隐式转换法:
有参构造函数:Person p4 = 10; ====== Person p4 = Person (10); -----//仅仅适合一个参数的构造函数;
复制构造函数:Person p5 = p4; ======== Person p4 = Person (p4);
初始化列表初始化属性 ------可以解决const成员变量(或者声明为引用的类成员)的赋值问题,如果不采用这种方法给const成员变量赋值,会报错(当然对于一些情况可以在声明时就赋值,但对于需要赋值来自于用户,就需要使用这种方法。)另外,初始化的顺序只和变量声明的顺序相关,而与列表初始化的顺序无关。
----只有构造函数
可以使用这样的方式初始化
Person(int i, int j, int k) : a(i), b(j), c(k){}
//就会给Person的三个属性abc赋值ijk;
当类方法不修改成员变量时,尽量将类方法声明成const : void show() const;
::: tip
只有当类没有声明任何构造函数的时,编译器才会自动的生成默认构造函数。一旦我们定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则类将没有默认构造函数
委托构造函数:即类的多个构造函数代码可能相同,故可以在一个构造函数类内使用其他构造函数----成员列表初始化的方法;
继承构造函数: 在派生类中使用 using baseclass :: baseclass ;
则派生类就可以使用基类的所有构造函数,但是对派生类中自己的特定版本,会覆盖掉基类的版本。
当然不仅限于构造函数,对基类的其他函数也可以使用该方法。当然继承来的构造方法,只能初始化基类的成员,若对所有成员都想初始化,则使用成员列表初始化语句。
(4)析构函数
没有返回值,不写void
;
函数名同类名,在名称前加~
;
析构函数不可以有参数,不可以有参数;
对象在销毁前,会自动调用析构函数,而且只会调用一次;
~Person(){};
×××一定要显式析构函数来释放类构造函数使用new分配的内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使不需要析构函数,也要提供一个虚析构函数。
(5) 转换函数----比如将类对象转换成int 或者double型
operator typeName(); typeName
是要转换的类型
注意: * 转换函数必须是类方法;*转换函数不能指定返回类型;*转换函数不能有参数;
example: 转换为double类型的函数原型 :
//class ---A
class A{
int a;
operator double();//声明
};
A::operator double()
{
return double(a);
}
在实现某一个类时,针对类的成员变量的初始化,赋值主要有三种形式,1.在声明变量的位置带上初值(类内初始值设定项)。2在构造函数中使用列表初始化。3在构造函数中进行赋值。首先变量都必须初始化。我们知道2一定是初始化,3一定是赋值。只有1不好确定是什么,但通过测试,可知,当没有列表初始化,在构造函数中直接打印的变量值就是1的初值。当有初始化列表,则打印出来的是2的值。因此,1有点类似函数的默认参数,若传参时不指定,就是默认参数的感觉。
对于类中const修饰的成员变量,肯定是要初始化的,1,2都可以实现。对于类中static修饰的成员变量,上述三种都不能行,必须在类外初始化(注意是初始化,类型+类名::变量名+值)
静态成员变量
类内声明(static int A;
),类外初始化(int Person::A = 0;
);----因为声明描述如何分配内存,但不分配内存
为什么不能在类的内部定义以及初始化static成员变量,而必须要放到类的外部定义? 因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含自己的静态成员,这是矛盾的。
静态成员函数: 在函数前加static;
特点:所有对象共享一个函数; 静态成员函数只能访问
静态成员变量和静态成员函数;
静态成员(变量)函数的调用: 1通过对象;2通过类名
class A{
private:
static int a;
int b;
public:
static int POW(int x){
return pow(a,x);
}
};
int A::a = 10;
常函数
const
修饰成员函数 ----常函数: (成员函数后加const;修饰的是this
指针,让指针指向的值也不可以修改):
特点:1常函数不可以修改成员属性;2成员属性声明时加关键字mutable后,在常函数中可以修改;
class A{
public:
void print() const{
cout<<""<<endl;
}
};
常对象:声明对象时前加const: 特点:常对象只能调用常函数; 不能修改普通的成员变量
(1)访问控制
| public | 使用public定义的成员,在整个程序内可被访问,public成员定义类的接口。 |
| private | 使用private定义的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。 |
友元函数
(2)友元
----(友元函数 ,友元类,友元成员函数)
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
以friend关键字标识。
友元不是类的成员,不受访问控制级别的约束。
友元函数:可以用来访问类的私有部分;
友元的声明仅仅制定了访问的权限,而非通常意义的函数声明。必须在友元之外再专门对函数进行一次声明。
----全局函数作友元:
1类中声明(在类的最上方,不用写在public下) friend void goodfriend(...);
2 类外定义 void goodfriend(){}
----类作友元: 在类中声明(具体位置无所谓,一般在最前面声明): friend class goodgay ;
则goodgay类的所有成员函数就可以访问该类的私有,保护成员了。
----成员函数作友元:在类中声明: friend void goodgay::visit();
则需要在前面声明好goodgay类和visit方法;
//友元函数 --两个example
class Time{
friend int operator*(int , const Time &);//友元函数声明
friend ostream & operator<<(ostream & os,const Time & );//友元函数声明
private:
int hour;
public:
Time(int a): hour(a){};
int operator*(int b);
};
int Time::operator*(int b)
{
return this->hour * b;
}
int operator*(int b,const Time &t)
{
return b * t.hour;
}
ostream & operator<<(ostream & os,const Time &t)
{
os<<t.hour<<" ";
return os;
}
int main(){
Time t(3);
cout<<t * 4<<endl;//12
cout << 4 * t <<endl ;// 12 //在没有友元函数下会报错
cout<<t<<t<<endl; //3 3
return 0;
}
//友元类
class TV
{
friend class Remote;//遥控器类 ---可以改变TV的一些状态
private:
bool state; //开关,true,false两种
public:
TV(bool b):state(b){};
void print();
};
void TV::print(){
cout<<this->state<<endl;
}
class Remote
{
public:
void modify(TV &t);//以TV类的引用为输入,表明是针对某一确定TV
};
void Remote::modify(TV &t)
{
t.state = t.state == true? false : true;
}
int main(){
TV t(true);
Remote remote;
t.print();
remote.modify(t);
t.print();
}
//成员函数做友元
//比较麻烦在定义顺序上;
/*//B中成员函数做A的友元
class A;//声明
class B{....};//声明
class A{....};/声明
实现A,B
*/
class TV;
class Remote
{
public:
void modify(TV &t);
};
class TV{
friend void Remote::modify(TV &t);//遥控器类方法 ---可以改变TV的一些状态
private:
bool state; //开关,true,false两种
public:
TV(bool b):state(b){};
void print();
};
void TV::print(){
cout<<this->state<<endl;
}
void Remote::modify(TV &t)
{
t.state = t.state == true? false:true;
}
int main(){
TV t(true);
Remote remote;
t.print();
remote.modify(t);
t.print();
}
运算符重载
限制条件:
1:重载后至少有一个操作数是用户定义的类型;
2:不能违反原来规则,原来是几元操作符,重载后还必须是几元操作符;
3:不能创建新运算符。
4:sizeof . :: ?:
等等不能被重载
加法运算符重载: 1成员函数重载;2全局函数重载 总结:对于内置数据类型的表达式的运算符是不能改变的;不要滥用运算符重载
左移运算符重载:<<(只能全局函数重载)
递增运算符重载:
赋值运算符重载:
关系运算符重载:
函数调用运算符重载:()—仿函数
class Person
{
friend ostream& operator<<( ostream &cout, Person &p);
friend Person operator+(Person &p1, Person &p2);
};
ostream& operator<<( ostream &cout, Person &p)
{
cout << "m_A = " << p.m_A << "m_B" << p.m_B;
return cout;
}
继承
继承的重要特点-------基类引用可以指向派生类对象
继承 :(父类中的所有成员属性都会被子类继承、父类的私有成员变量只是访问不到,但是还是继承了)
继承方式
公共继承(子类的权限和父类相同);保护继承(会将父类的公共权限提升到保护权限);私有继承(会将父类的公共权限和保护权限提升到私有权限)
对于父类的私有成员,不管哪种继承都不能访问,但不管哪种继承其实对父类三种权限下的东西都拿到了,只不过有些不能访问而已,而继承的权限只是用来限制子类继承得到的东西的权限。
子类对父类成员的处理
//example
class People{
private:
string name;
int age;
public:
People() = delete;
People(string s, int i):name(s),age(i){};
void print()
{
cout<<this->name<<" "<< this->age<<endl;
}
};
class Student : public People{
private:
int score;
public:
Student() = delete;
Student(string s, int i, int j):People(s,i),score(j){};
void print1()
{
cout<<this->score<<endl;
}
};
int main(){
People p1("wang",28);
Student s1("li",18,100);
p1.print();//wang 28
s1.print(); //li 18
s1.print1();//100
//注意的事:子类不能直接访问到父类的私有成员,比如这里,s1虽然也有name,age,但是在子类中是访问不到的,可以在父类中提供公共接口。
//若子类成员函数名和父类一样,如上,将print1 改为print,则调用如下:
s1.People::print();//li 18
s1.print();//100
}
这里使用了初始化成员列表的方法,
首先创建基类对象;
派生类构造函数通过成员初始化列表将基类信息传递给基类构造函数;
派生类构造函数应该初始化派生类新增的数据成员。
注意:通常 应该将基类和派生类的声明放在一个头文件里面。(非必须)
基类方法使用总结:
1派生类对象自动使用继承来的基类方法,如果派生类没有重新定义这些方法;
2派生类的构造函数自动调用基类的构造方法;
3派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数;
4派生类构造函数显式调用成员初始化列表中指定的基类构造函数;
5派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法;
派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数;
派生类和基类关系:
1派生类对象可以使用基类的非私有方法;
2基类指针和引用可以在不进行显式转换的情况下指向派生类对象;—这样就可以在某些参数是基类指针或者引用的函数传递实参时传递派生类或者可以在基类对象调用
3基类指针或者引用只能调用基类的方法,不能调用派生类的方法,因为派生类方法中可能用到派生类的新增成员,基类调用就会出错
4,相比于2,不可以将基类对象和地址赋值给派生类引用或指针。
继承中先调用父类构造函数,在调用子类构造函数,析构顺序相反。
继承同名成员处理方式
1访问子类同名成员,直接访问即可(如S.m_A
);
2访问父类同名成员,需要加作用域(S.fulei::m_A
);
继承的同名成员函数处理方式:
1访问子类同名成员函数,直接访问即可(如S.func()
);
2访问父类同名成员,需要加作用域(S.Base::func()
);
如果子类中出现了和父类同名的成员函数,子类的同名成员函数会隐藏掉父类的所有·同名成员函数(即函数重载)
继承同名静态成员(可以通过对象(同上)或者通过类名)处理方式:
1访问子类同名静态成员,直接访问(如 Son::A
);
2访问父类静态同名成员,需要加作用域(如Son::Base::A
-----第一个::表示通过类名调用,第二个::表示访问父类作用域下);
继承同名静态成员函数处理方式(通过对象调用同上或者通过类名调用):
1访问子类同名成员函数,直接访问即可(如Son::func()
);
2访问父类同名成员,需要加作用域(Son::Base::func()
);
如果子类中出现了和父类·同名的静态成员函数,子类的同名成员函数会隐藏掉父类的所有·同名成员函数(即函数重载)
多继承中如果多个父类中出现同名情况,子类使用时需要加作用域。
如果子类没有显示的调用父类的构造函数,那么默认会调用父类无参的构造函数!!!
如果父类只提供了有参数的构造函数,那么子类在默认情况下调用父类的无参构造函数时就会报错!
class People{
private:
string name;
int age;
public:
int A;
static int B;
People() = delete;
People(string s, int i):name(s),age(i){A = 1;};
void print()
{
//cout<<this->name<<" "<< this->age<<endl;
cout<<"父类普通函数"<<endl;
}
static void print_static()
{
//cout<<B<<endl;//注意静态函数只能访问静态变量,静态变量没有this指针
cout<<"父类静态函数"<<endl;
}
};
int People::B = 10;
class Student : public People{
private:
int score;
public:
int A;
static int B;
Student() = delete;
Student(string s, int i, int j):People(s,i),score(j){};
void print()
{
//cout<<this->score<<endl;
cout<<"子类普通函数"<<endl;
}
static void print_static()
{
cout<<"子类静态函数"<<endl;//注意静态函数只能访问静态变量,静态变量没有this指针
}
};
int Student::B = 11;
int main(){
Student s1("li",18,100);
//普通同名函数
s1.print();
s1.People::print();
//静态同名函数
s1.print_static();
Student::print_static();
s1.People::print_static();
People::print_static();
//静态成员变量
cout<<s1.B<<endl;
cout<<s1.People::B<<endl;
cout<<Student::B<<endl;
cout<<Student::People::B<<endl;
}
//子类普通函数
//父类普通函数
//子类静态函数
//子类静态函数
//父类静态函数
//父类静态函数
//11
//10
//11
//10
多态
静态多态(即函数重载,运算符重载 -----静态多态的函数地址早绑定,编译阶段确定函数地址)和动态多态(派生类和虚函数—动态多态的函数晚绑定,运行阶段确定函数地址)
虚函数:就是一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类的对象时,调用虚函数时,实际调用的是继承版本.
在函数声明时,最前面加virtual
example: virtual void functionname();
在基类中将方法声明为虚函数,则在派生类中该方法将自动转成虚方法。
虚函数一般是在类继承中,多态时使用,即子类和父类有相同的方法时使用虚函数。
但当派生类提供的实现版本不同于基类的虚方法时,不是重载基类的该方法(这里的特征标是相同的),而是将继承来的基类的方法隐藏,。因此想要重新定义继承来的基类方法,必须保证特征标相同;但对返回值是基类引用或指针
的,可以改为派生类,这种特性被称为返回值类型协变。其次若基类将某方法重载了,则派生类若重写了其中一个,则需要将他们都重写,不然会隐藏没重写的。当你想覆盖基类的虚方法时,可以在派生类该虚函数处使用override
,若特征标不同,则会报错;同理,若想禁止覆盖,则可以使用final
。
例如:派生类虚函数:virtual void f(...) const override{...};
当类中存在虚函数时,需要采用动态联编。
动态多态
满足条件:
1存在继承关系
2子类(virtual关键字可不写)需要重写父类的虚函数。
动态多态调用条件
:父类引用或指针指向子类对象。(如函数的形参是父类引用或指针,函数调用是传入子类对象)
如果使用虚函数,且通过引用或指针而不是对象调用该函数,程序将根据引用或指针指向的对象的类型来选择方法(动态联编),如果没有使用虚函数,则根据引用或者指针类型选择方法。
当一个类作为基类时,且存在动态联编,则基类的析构函数应该是虚函数。
A* d = new B();(假定A是基类,B是从A继承而来的派生类)如果析构函数不是虚函数,只会调用父类的析构函数,清除d中的父类部分指向的内存。
构造函数和友元函数不能是虚函数。
class People{
private:
string name;
int age;
public:
People() = delete;
People(string s,int a):name(s),age(a){};
virtual void print()
{
cout<<"People的print函数调用"<<endl;
}
virtual ~People(){};
};
class Man :public People
{
public:
Man() = delete;
Man(string s,int a):People(s,a){};
void print()
{
cout<<"Man的print函数调用"<<endl;
}
};
class Woman :public People
{
public:
Woman() = delete;
Woman(string s,int a):People(s,a){};
void print()
{
cout<<"Woman的print函数调用"<<endl;
}
};
void func(People* p)
{
p->print();
}
int main()
{
People *p = new People("ddd",13);
People *m = new Man("mmm",19);
People *w =new Woman("dfsfr",44);
p->print();
m->print();
w->print();
cout<<" ***************** "<<endl;
func(p);
func(m);
func(w);
delete p;
delete m;
delete w;
}
//People的print函数调用
//Man的print函数调用
//Woman的print函数调用
//若去掉People中print函数前面的virtual,则打印出来的全是People
// *****************
//People的print函数调用
//Man的print函数调用
//Woman的print函数调用
}
protected和private的区别:在派生类中,派生类成员可以直接访问基类的保护成员(不用通过基类的公有成员方法),但不能直接访问基类的私有成员。对类外的只能使用公有类成员方法来访问保护成员。对派生类而言,基类的保护成员相当于公有成员。
继承时,父类的析构函数是否为虚函数?构造函数能不能为虚函数?为什么
?
父类的析构函数是虚函数: (1)当子类中有属性开辟到堆区并且子类对象指向父类的指针或引用时,若析构函数不为虚函数,在清理对象时,只会调用父类的析构函数,释放父类指针或者引用指向的内存,而不会调用子类的析构函数,导致内存泄漏。(2)当析构函数为虚函数时,在清理对象时,子类会调用子类的析构函数,父类调用父类的析构函数,正确释放内存。
构造函数不能为虚函数:(1)当子类创建对象时,会首先调用子类的构造函数,然后再调用父类的构造函数(即子类不继承父类的构造函数),因此,构造函数为虚函数没有意义。(2)虚函数的调用,要用到虚函数表指针,指针属于类的对象上
,实例化对象需要调用构造函数,如果构造函数为虚函数,前后相矛盾。
抽象基类(ABC)
将两个类或者多个类的共性抽象出来,放在一个ABC中,然后在ABC派生出类。要成为真正的ABC,必须包含一个纯虚函数
好处是可以使用ABC的指针数组同时管理他的派生类。且抽象基类至少包含一个纯虚函数
纯虚函数:(声明结尾为=0;example:virtual void functionname() const = 0;
)
抽象类–1不能实例化对象,2子类必须重写父类的纯虚函数,否则也属于抽象类
包含纯虚函数的只能用作基类,不能实例化对象;
原型中使用=0会指出类是一个抽象基类,在类中可以不定义该函数(当然也可以定义)
class People{
private:
string name;
int age;
public:
People() = delete;
People(string s,int a):name(s),age(a){};
virtual void print() = 0;
virtual ~People(){};
};
class Man :public People
{
public:
Man() = delete;
Man(string s,int a):People(s,a){};
void print()
{
cout<<"Man的print函数调用"<<endl;
}
};
class Woman :public People
{
public:
Woman() = delete;
Woman(string s,int a):People(s,a){};
void print()
{
cout<<"Woman的print函数调用"<<endl;
}
};
void func(People & p)
{
p.print();
}
void func(People* p)
{
p->print();
}
int main()
{
//People p = new People("dsf",12);//会报错,抽象基类不能创建对象
People *m = new Man("mmm",19);
People *w =new Woman("dfsfr",44);
m->print();
w->print();
cout<<" ***************** "<<endl;
func(m);
func(w);
cout<<" ***************** "<<endl;
Man m1("dw",13);
Woman w1("dwdew",33);
func(m1);
func(w1);
}
虚函数实现动态多态的原理、虚函数与纯虚函数的区别
:
当类中包含虚函数时,实际上类会有一个指向虚函数表头的指针vfptr(4字节),虚函数表按照虚函数的声明顺序存在表中。当派生类重写了某个基类虚函数,那么派生类中的虚函数表,会用重写的虚函数地址覆盖基类原来的虚函数地址,其他的保持不变,若派生类也有自己的虚函数,则加到基类的虚函数地址的后面。当基类指针指向基类对象是,基类对象的vfptr指向基类的虚函数表,就会运行基类的虚函数;而基类指针指向派生类对象时,派生类对象的vfptr指向的是派生类的虚函数表,就会运行派生类的对应虚函数;这样就实现根据基类指针指向不同的对象,从而调用不同的函数体,就会有不同的表现形式,即实现了多态。
虚函数:父类提供了虚函数的声明和实现,为子类提供了默认的函数实现,若子类重写父类的虚函数,就可以实现子类的特殊化。使用virtual关键字
纯虚函数:纯虚函数在基类只声明不提供实现,包含纯虚函数的类,称为抽象类,抽象类不能生成对象,派生类必须对基类声明的纯虚函数进行定义(不要在带=0,否则也是抽象类)。纯虚函数是用来规范派生类的行为和接口。在虚函数的基础上,在最后加"=0"
区别
:虚函数在基类中必须有自己的实现,在子类中可以有也可以没有该虚函数的实现,若有,则根据调用对象调用不同版本的虚函数,若没有,调用基类的虚函数。纯虚函数在基类必然没有实现,在子类中必须实现该纯虚函数。同时包含纯虚函数的类称为抽象类,抽象类不能实例化对象。定义纯虚函数就是为了让基类不能实例化。
继承与动态内存分配:当基类使用类动态内存分配,则本身需要重新定义赋值运算符和复制构造函数和析构函数,派生类如果没有使用动态内存分配,则派生类不需要重新定义,如果派生类也使用类动态内存分配,则派生类也需要重新定义。
explicit用来防止由构造函数定义的隐式转换。
私有继承:private是默认值,可以省略。
空类有哪些函数?空类的大小
?
默认构造函数,默认拷贝构造函数,默认赋值构造函数,取址运算符,取址运算符const,默认析构函数。空类的大小为1
,当类不包含虚函数和非静态数据成员时,其对象大小也为1,类的大小为类的非静态成员变量数据类型大小之和,为了优化存取效率,进行了边缘调整,与类的成员函数无关
。如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable,在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。而虚函数本身和其他成员函数一样,是不占用对象的空间的。
为什么不为0
?为了确保两个不同对象的地址不同,必须如此。类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以,空类的sizeof为1,而不是0.
私有继承:—实现has-a的关系 -----尽量少用
class Student:private std::string,private std::valarray<double>{
public:
Student(const std::string &a,const ArrayDb&b):string(a),valarray<double>(b){};
};
//访问基类方法:使用类名和作用域解析运算符来调用基类的方法:ArrayDb::sum() // ArrayDb::size()访问基类对象:私有继承没有name对象,怎麽得到name对象呢?--强制向上类型转换:
const string & Student::Name() const{
return (const string &) *this;
}//该方法返回一个引用,指向调用该方法的Student对象中的继承来的string对象。
保护继承:将私有继承的private换成protected 与私有继承的主要区别在于从派生类派生出另一个类时,私有继承下,第三代类不能使用基类的接口,而保护继承可以。
多重继承:
example :class Waiter : public Work{};
class Singer : public Work{};
class SingWaiter : public Waiter, public Singer{…};
带来的问题
1:SingWaiter 将包含两个Work组件;
解决方法1: SingWaiter ed; Work × pw = &ed 将有二义性; 故 Work × pw1 = (Waiter ×) &ed; Work × pw2 = (Singer ×) &ed;
解决方法2: 虚基类:----复杂
class Waiter : public virtual Work{}; //顺序无关
class Singer : virtual public Work{};
class SingWaiter : public Waiter, public Singer{…};
现在SingWaiter对象只包含Worker对象的一个副本。本质来说,继承的Singer和Waiter对象共享一个Worker。
使用虚基类后,需要改变类构造函数