C++基础(1)
C++基础(2)
C++基础(3)
目录
7.类
使用类来定义自己的数据类型。
7.1 设计类
7.1.4 构造函数
通过一个或几个特殊的成员函数来控制器对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化对象的数据成员。
- 构造函数不能被声明称const。
- 已经提供了一个构造函数,那么编译器就不会在自动生成默认构造函数。所以在定义对象的时候,可能不能初始化。
7.1.4.1某些类不能依赖于合成的默认构造函数
对于一个普通的类来说,必须定义它自己的默认构造函数:
- 编译器只有在发现泪不包含任何构造函数的情况下,才会替我们生成一个默认构造函数。一旦定义了一些其他的构造函数,那么除非我们在定义一个默认构造函数,否则将没有默认构造函数。
- 合成某些类来说,合成的默认构造函数可能执行错误的操作。如果定义在块中的内置类型或复合类型的对象呗默认初始化,则他们的值将是未定义的。如果类内包含了内置类型或者复合类型,则只有当这些成员全部都被赋予了类内初始值时,这些类才不适合使用合成的默认构造函数。
- 有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器无法初始化改成员。
7.1.5 拷贝、赋值和析构
除了定义的对象如何初始化外,类还需要控制拷贝、赋值和销毁对象时发生的行为。
- 对象在几种情况下会拷贝:
- 初始化变量以及以值的方式传递或返回一个对象等。
- 当我们使用赋值运算符会发生对象赋值操作
- 当对象不再存在的时执行销毁的操作
- 一个局部对象会在创建它的块结束时被销毁。
7.1.5.1某些类不能依赖于合成的版本
当我们不主动定义这些操作时,则编译器会替我们合成它们。但要清楚的一点是,对于某些泪来说合成的版本无法正常工作,特别是,当类需要分配对象之外的资源时,合成的版本常常会失效。特别是动态分配和管理动态内存。如果类包含vector或string成员,则其拷贝、赋值和销毁的合成版本能够正常工作。
7.2 访问控制与封装
其中struct和class唯一的区别就是默认的访问权限
- struct关键字,在定义一个访问说明符之前的成员是public的。相反,如果我们使用class关键字,则这些成员是private。
7.2.1友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
- 友元声明只能出现在类的内部,在类的内出现的位置不限。
7.2.1.1 友元声明
- 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们必须在友元声明之外在专门对函数进行一次声明。
7.3 类的其他特性
7.3.1 类成员再探
7.3.1.1 定义一个类型成员
7.3.1.2 令成员作为内联函数
在类中,常有一些规模较小的函数适合于被声明成内联函数。定义在类内部的成员函数是自动inline的。
7.4 类的作用域
7.5 构造函数再探
7.6 类的静态成员
- 定义静态成员:既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字。
8 继承与派生
8.1 继承与派生的概念
8.1.1 概念
- 单继承:每个派生类只有一个基类
- 多继承:一个派生类有两个或两个以上的基类
8.1.2 派生类的实现
- 如果没有显示给出继承方式,系统默认是私有继承。
- 派生类的实现方式:
- 吸收基类成员:基类的全部成员被派生类继承,作为派生类成员的一部分。
- 改造和限制:派生类根据实际情况对继承自基类的某些成员进行限制和改造。
- 对基类的成员访问限制通过继承方式,就是修饰继承的
- 对基类成员进行改造通过同名覆盖来实现,如果是成员函数,参数列表也要一样,不然会被看成是重载。
- 继承方式
- 添加新成员:可以根据自己需要添加新成员
8.1.3 继承与组合
- 组合与继承的关系:
- 继承是一般类与特殊类的关系:例如汽车是交通工具的一种
- 组合:是一部分关系:例如发动机是汽车的一部分,不能说发动机是汽车。
8.2 继承权限
- 分为派生类和派生类的对象。
- 私有:只能在类的内部,类的对象也不可以。
- 私有成员,可以通过共有的成员函数去访问
8.2.1 公有继承
- 基类的共有成员在派生类中仍然为公有成员,可以由派生类对象和派生类成员函数直接访问。
- 基类中私有成员在派生类中还是私有成员,派生类和派生类的对象都无法直接访问
- 保护成员,在派生类中仍是保护成员,可以通过派生类的成员访问,但不能有派生类的对象直接访问。
8.2.2 私有继承
- 基类的公有成员和保护成员被继承后,作为派生类的私有成员,即基类的共有成员和保护成员被派生类吸收后派生类的其他成员函数可以直接访问他们,但是在类的外部,不能通过派生类的对象访问他们。
- 基类的私有成员,不论是派生类还是派生类的对象都不可以访问,
- 经过私有继承后,所有基类的成员都成为派生类的私有成员或不可以访问成员,如果进一步派生的,基类的全部成员将无法在新的派生类中被访问。因此继承之后,基类的成员在也无法在以后的派生类中发挥作用,实际是相当于终止了基类的继承派生。
8.2.3 保护继承
- 基类的公有成员和保护成员被继承后作为派生类的保护成员
- 基类的私有成员在派生类中不能被直接访问
8.3 派生类的构造函数与析构函数
- 如果基类定义l带有形参表的构造函数时,派生类就应当定义构造函数,提供一个将参数传递给基类构造函数的途径。
- 派生类的析构函数只负责把派生类新增的非对象成员的清理工作做好就够了。
8.3.1 单继承的构造与析构
- 如下所示:下面 tube中的 Circle Incircle; 是组合类。
- 单继承时,派生类构造函数调用的一般次序如下:
- 调用基类。
- 如果派生类中有新增的对象,则调用派生类对象的基类。
- 最后才是派生类自己的初始化。
class Point
{
public:
Point(int X = 0, int Y = 0)
{
this->X = X;this->Y = Y;
cout << "Point(" << X << "," << Y << ") constructing..." << endl;
}
~Point()
{
cout << "Point(" << X << "," << Y << ") Deconstructing..." << endl;
}
private:
int X, Y, z;
};
class Circle :protected Point
{
public:
Circle(int R, int X, int Y) :Point(X, Y)
{
readius = R;
cout << "Circle constructing...readius"<<R << endl;
}
~Circle()
{
cout << "Circle Deconstructing...readius" << readius << endl;
}
protected:
double readius;//半径
};
class tube :protected Circle
{
public:
tube(double H, double R1, double R2 = 0, int X=0, int Y = 0) :Incircle(R2, X, Y), Circle(R1, X, Y)
{
height = X;
cout << "tube constructing...readius" << height << endl;
}
~tube()
{
cout << "tube Deconstructing...readius" << height << endl;
}
private:
double height;
Circle Incircle;
};
///输出
Point(0,0) constructing...
Circle constructing...readius10
Point(0,0) constructing...
Circle constructing...readius5
tube constructing...readius0
tube Deconstructing...readius0
Circle Deconstructing...readius5
Point(0,0) Deconstructing...
Circle Deconstructing...readius10
Point(0,0) Deconstructing...
8.4 兼容类型
兼容类型是指在共有派生的情况下,一个派生类对象可以作为基类的对象来使用的情况。由于派生类得到了基类的所有成员,所以可以把派生类对象作为基类对象来处理。
反过来,如果试图通过基类指针引用那些只有在派生类中才有的成员,则会出错因为派生类中新增或改造后的成员是基类所没有的。
- 派生类对象可以赋值给基类对象
- 派生类对象可以初始化基类的引用
- 派生类对象的地址可以复制给指向基类的指针
- 总结:
- 左边是基类,右边是派生类。基类可以指向或引用派生类,同时派生类可以赋值给基类。
- 因为基类所占空间(成员少),所以可以指针地址大 的
- 也就是形参用基类可以传入派生类。这里只是让派生类的数据使用基类的成员函数。
- 如果想用派生对象自己的成员函数,要加virtual关键字。注意和上面的区别。
//不同的对象,都可以使用基类的函数,把派生类的数据展示出来。
void display(Point p)
{
p.ShowXY();
}
int main()
{
Point p(1, 1);
Circle cir(15, 20, 20);
Cylinder Cy(15,300, 300, 50);
display(p);
display(cir);
display(Cy);
return 0;
}
//也可以通过基类指针指向子类
void display(Point *p)
{
p->ShowXY();
}
int main()
{
Point p(1, 1);
Circle cir(15, 20, 20);
Cylinder Cy(15,300, 300, 50);
display(&p);
display(&cir);
display(&Cy);
return 0;
}
8.5 多继承
是指派生类具有两个或两个以上的直接基类。
8.5.1 多继承的构造与析构
与单继承相比,多继承派生类的基类数目多一些,构造函数的调用先后顺序要复杂些。
- 调用各基类构造函数,各基类构造函数调用顺序按照基类被继承时声明的顺序,从左向右依次进行。
- 调用内嵌成员的构造函数。成员对象的构造函数调用顺序按照他们在类中定义的顺序依次进行。
- 调用派生类的构造函数。
8.5.2 二义性问题
如果多个基类中拥有同名的成员,那么派生类在继承各个基类的成员之后,当调用该类派生类成员时, 由于该成员标识不唯一,出现二义性。
- 解决方法:
- 成员名限定:Car::show(),Wagon::show()
- 成员重定义:在派生类中新增一个与基类中成员相同的成员,由于同名覆盖,程序将自动选择派生类新增的成员。
8.6 虚基类
从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置成虚基类。
8.6.1 虚基类定义
定义成虚基类后,在派生过程中,始终和派生类在一起,共同维护一个基类子对象的拷贝。
否则,继承一个就拷贝一个基类作为自己的成员,多个类的话,将会有很多复制基类。
class 派生类名:virtual 继承方式
8.6.2 虚基类的构造与析构
9 多态
动态是指同样的消息被不同的类型的对象接受时导致完全不同的行为。
- 运算符、重载、函数模板。都是C++多态性的体现。
9.2 运算符重载
9.2.1 运算符重载机制
所谓重载就是使用函数重载机制,实现的。
- 例如:
- 使用8+9 则是第一个,
- 使用8.8+9.9则是使用第二个
int operator +(int ,int); int operator +(double,double);
9.2.2 运算符重载规则
9.2.2 .1.
- C++运算符除了以下5个运算符外,其余部分都可以被重载。
- .
- *
- ::
- 三目选择运算符
- sizeof 运算符
9.2.2.2 运算符发重载规则
- 优先级与结合性不变
- 不能改变原操作数的个数
- 不能重载C++中没有的运算符
- 不能改变原有语义
9.2.2.3 运算符发重载规则
- 运算符重载一般有两种形式:
- 重载为类的成员函数
- 重载类为类的非成员函数:
- 非成员函数通常用友元。
9.2.3 运算符两种重载
运算符重载分为两种:
- 分为友元函数重载和类成员函数重载。
- 他们的不同之处为:
- 定义不一样,语法不一样。
- 当为双目运算符时,友元函数是两个形参。类成员函数是一个形参,另一个是本身。
运算符之间的不同
- 一般单目运算符重载为类的成员函数;双目运算符则最好重载为类的友元函数。
- 一些双目运算符不能重载为类的友元函数
- = 、()、[]、->
- 类型转换函数只能定义为一个类的成员函数,而不能定义为类的友元函数。
- 如果一个运算符的操作要修改对象的状态,选择重载为成员函数较好。
- 如果运算符所需要的操作数(尤其是第一个操作数),希望有隐式类型转换,则只能使用友元函数
9.2.4 重载为类的友元函数
运算符之所以要定义为友元函数,因为这样可以自由的访问该类的任何成员和数据。
friend 函数类型 operator 运算符(形参表)
{
函数体;
}
重载运算符为类的友元函数进行复数类数据运算
/两种,第一种是在类内实现,另一种是在类外实现
#include <iostream>
using std::cout;
using std::endl;
class Complex
{
public:
Complex(double real=0.0,double image=0.0)
{
this->real = real; this->image = image;
}
void display()
{
cout << "(" << real << "," << image << ")" << endl;
}
//重载+ 为友元函数
//重载运算符+ 的函数实现
friend Complex operator +(Complex A, Complex B)
{
return Complex(A.real+B.real,A.image+B.image)
}
~Complex();
//重载 为友元函数
friend Complex operator-(Complex A, Complex B);
private:
double real; //实部
double image; //虚部
};
//重载 的实现
Complex operator-(Complex A, Complex B)
{
return Complex(A.real - B.real, A.image - B.image);
}
9.2.5 重载为类的成员函数
运算符函数重载为类的成员函数,这样运算符可以自由的访问本类中的数据成员。重载运算符的语法为
返回类型 类名::operator 运算符(形参表)
{
函数体;
}
- 双目运算符重载为类的成员函数时,函数只显示说明一个参数,该参数是运算符的右操作数。
- 前置单目运算符重载为类的成员函数时,不需要显示说明参数,即函数没有形参。
- 后置单目运算符重载为类的成员函数时,为了与前置单目运算符区别,函数要带有一个整数形参。
using std::cout;
using std::endl;
class Complex
{
public:
Complex(double real=0.0,double image=0.0)
{
this->real = real; this->image = image;
}
void display()
{
cout << "(" << real << "," << image << ")" << endl;
}
//重载+ 为友元函数
//重载运算符+ 的函数实现
~Complex();
//重载 为友元函数
Complex operator-(Complex B);
private:
double real; //实部
double image; //虚部
};
//重载 的实现
Complex Complex:: operator-( Complex B)
{
return Complex(real - B.real, image - B.image);
}
9.2.6典型的运算符重载
1. ->
class PComplex
{
public:
PComplex(Complex *PC = NULL)
{
this->PC = PC;
}
Complex *operator->()
{
static Complex NullComplex(0, 0);
if (PC==NULL)
{
return &NullComplex;
}
return PC;
}
~ PComplex();
private:
Complex *PC;
};
main()
{
PComplex P1; //这里正常要报错的,因为指针没有初始化
P1->display();
}
2. []
9.3 虚函数
9.3.1 静态联编与动态联编
调用重载函数时,编译器根据调用时参数的类型与个数在编译时静态联编,将调用体与函数绑定。在编译时。
静态编译是在编译时,根据指针和引用的类型而不是根据实际指向的目标确定的函数。
动态联编则是在程序运行的过程中,根据指针与引用的实际指向的目标调用对应的函数,也就是在程序运行时才决定如何动作。
9.3.2 虚函数的定义与使用
virtual 函数类型 函数 (形参表) { 函数体; }
- 注意:
- 当在派生类中未重新定义虚函数,虽然虚函数被派生类集成,但通过积基类、派生类类型指针、引用调用虚函数时,不实现动态联编,调用的是基类的虚函数。
- 当派生类中定义了虚函数的重载,但并没有重新定义虚函数时,与虚函数同名的重载函数覆盖了派生类中的虚函数。此时试图通过派生类对象、指针、引用调用派生类的虚函数将会产生错误。
- 虚函数不能是静态成员函数,也不能是友元函数。因为静态成员函数和友元函数不属于某个对象
- 内联函数是不能在运行中动态确定其位置,即虚函数在类的内部定义,编译时,仍将其看作非内联。
- 只有类的成员函数才能说明为虚函数,虚函数的声明只能在出现在类的定义中。因为虚函数仅适合于有继承关系的类的对象,普通函数不能声明为虚函数。
- 构造函数不能是虚函数,析构函数可以是虚函数而且通常声明为虚函数。
- 使用虚函数是动态联编的基础,要实现虚函数,应满足三个条件
- 满足类型兼容性
- 在基类中定义虚函数,并且在派生类中要重新定义虚函数
- 要由成员函数或者执政、引用访问虚函数。
- 虚函数
- 虚函数有函数体
- 虚函数在静态联编时当册灰姑娘一般函数成员使用
- 虚函数可以派生,如果在派生类中没有重新定义虚函数,虚函数就充当派生类的虚函数。
9.3.3 虚析构函数
虚析构函数定义:
virtual ~类名();
- 当基类的析构函数被声明为虚函数,则派生类的析构函数,无论是否使用virtual关键字,都自动成为虚函数。
9.4 抽象类
抽象类是一种特殊的类,是为了抽象的目的而建立的,建立抽象类,就是为了通过它的多态使用其中的成员函数。
9.4.1 纯虚函数
是一个在基类中说明的虚函数,他在该基类中没有定义具体的实现,要求派生类根据实际需要定义函数实现。
virtual 函数类型 函数名(参数表)=0;
实际上,它与一般虚函数成员的原型在书写格式上的不同就在于后面加上了"=0";
- 纯虚函数根本就没有函数体,而空虚函数的函数体为空。
- 纯虚函数所在的类是抽象类,不能直接进行实例化,空虚函数所在的类是可以实例化的。
- 他们的共同特点是都可以派生出新的类,然后在新类中给出新的虚函数的实现,而且这种新的实现可以具有多态特征。
9.4.2 抽象类与具体类
- 抽象类只能作为其他类的基类使用,抽象类不能定义对象,纯虚函数的实现由派生类给出。
- 派生类仍可以不给出所有基类中纯虚函数的定义。继续作为抽象类;如果派生类给出所有纯虚函数的实现,派生类就不再是抽象类而是一个具体类,就可以定义对象。
- 抽象类不能用作参数类型、函数返回值或强制类型转换
- 可以定义一个抽象类的指针和引用。通过抽象类的指针和引用,可以指向并访问各派生类成员。