一、面向对象程序设计的基本特点
面向对象方法中的抽象,是指对具体对象(问题)进行概括,抽出这一类对象的公共性质并加以描述的过程。主要包括数据抽象和行为抽象。
1、封装
封装就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的函数代码进行有机地结合,形成“类”,其中数据和函数都是类的成员。
在代码中,类声明中的 { },里面的内容其实就是实现封装。
2、继承
继承是C++中支持层次分类的一种机制,允许程序员在保持原有类特性的基础上,进行更具体的说明。通过类的这种层次关系可以很好的反映出特殊概念与一般概念的关系。
3、多态
多态是指一段程序能够处理多种类型对象的能力。可以通过强制多态、重载多态、类型参数化多态、包含多态四种方式来实现。
- 强制多态——强制转换
- 重载多态——给同一个名字赋予不同的含义,如函数重载、运算符重载
- 类型参数化多态——通过模板来实现,函数模板、类模板
- 包含多态——通过虚函数来实现
二、类和对象
1、类的定义:
class 类名称
{
public:
公有成员(外部接口)
private:
私有成员
protected:
保护型成员
}
2、类成员的访问控制
- 公有类型(public)成员定义了类的外部接口
- 私有类型(private)成员只能被本类的成员函数访问,来自外部的任何访问都是非法的。
- 保护类型(protected)成员与私有成员类似,差别在于保护类型成员可以被子类访问。
3、对象
类是一种抽象机制,描述了一类实物的共同属性和行为。类的对象就是该类的某一特定实体(也称实例)。
4、类的成员函数
1)成员函数的实现
函数的原型声明要写在类体里面,而函数的具体实现是写在类定义外的,因此实现类的成员函数要指明类的名称,具体形式如下:
返回值类型 类名::函数成员名(参数表)
{
函数体
}
2)成员函数调用中的目的对象
调用一个成员函数与调用普通函数的差异在于,需要使用“.”操作符指出调用所针对的对象,这一对象在本次调用中称为目的对象。例如类A有一个B函数,a是A的一个实例,a.B(),调用B函数时,a就是这一调用过程的目的对象。
在成员函数中可以不使用“.”操作符合,直接引用目的对象的数据成员。
3)内联成员函数
为了提高运行时的效率,对于较简单的函数可以声明为内联形式。
内联成员函数有两种声明方式:隐式声明和显式声明。
- 隐式声明:直接将函数体放在类体内。
- 显式声明:采样inline关键字显式声明,即在函数体实现时,在函数返回值前加上inline,在类定义中不写函数体。
class A
{
public:
void func1(){
cout<< "这是隐式内联函数"<<endl;
}
}
class B
{
public:
void func1();
}
inline void B::func1(){
cout<< "这是显式内联函数"<<endl;
}
三、构造函数和析构函数
1、构造函数
构造函数的作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。所以说构造函数在对象被创建的时候被自动调用。
调用时无需提供参数的构造函数称为默认构造函数。
2、委托构造函数
一个委托构造函数使用它所属的其他构造函数执行它自己的初始化过程,也就是说它把自身的一些(或全部)职责委托给了其他构造函数。
Clock(int hour,int min, int second){ //构造函数
this.hour = hour;
this.min = min;
this.second = second;
}
Clock():Clock(0,0,0){} //构造函数
//使用第二个构造函数时,第二个构造函数会委托第一个构造函数来完成数据成员的初始化,
//所以会先用(0,0,0)执行第一个构造函数。再执行第二个构造函数,只不过这个第二个构造函数是空的
3、复制构造函数
复制构造函数(也叫拷贝构造函数),其形参是本类的对象的引用。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。
如果我们没有定义这个复制构造函数,系统就会在必要的时候自动生成一个隐含的复制构造函数。这个隐含的构造函数的作用是,把初始化对象的每个数据成员的值都复制到新建立的对象中。
一般来说类里面有指针的时候,要自己定义一些拷贝构造函数,不然会让很多变量指向同一个内存。
class 类名
{ public :
类名(形参);//构造函数
类名(类名 &对象名);//拷贝构造函数
...
};
类名:: 类名(类名 &对象名)//拷贝构造函数的实现
{ 函数体 }
复制构造函数会在下面3种情况下会被调用。
- 当用类的一个对象去初始化该类的另一个对象时。
- 如果函数的形参是类的对象,调用函数时,进行形参和实参结合时。
-
如果函数的返回值是类的对象,函数执行完成返回调用者时。
4、析构函数
析构函数的作用是用来完成对象被删除前的一些清理工作。构造函数是在对象的生命周期即将结束的时刻被自动调用的。
析构函数的名称是在类名前加一个"~"符号,没有返回值,也不接受任何参数。
5、移动构造函数
左值是位于赋值语句左侧的对象变量,右值是赋值语句右侧的值,不依附于对象。
对持久存在变量的引用,称之为左值引用。对短暂存在可移动的右值的引用称之为右值引用。
//右值引用的格式
类型 && 引用名 = 右值表达式;
//左值引用,就是最普通的那个引用
类型 & 引用名 = 左值表达式;
float n = 6; //n是一个变量
float &lr_n = n; //对变量n的左值引用
float &&rr_n = n; //错误,等号左边是一个右值引用,绑定到左值n上
float &&rr_n = n * n; //乘法的结果是一个值,不是一个变量或者对象,可以将结果右值绑定到右值引用
float &lr_n = n * n; //错误,不能将左值引用绑定到乘法结果右值。
float n = 10;
float &&rr_n = std::move(n);//move函数可以将左值对象移动为右值。
复制构造函数是通过复制的方式构造新的对象,很多时候复制之后就被销毁。移动构造函数就可以移动已有对象,而非复制对象,可以大大提高性能。
class A
{
string s;
A(A &&a) noexcept //告知编译器不会抛出异常
: s(std::move(a.s)){ } //移动构造函数
}
6、default、delete函数
default可以显示要求编译器自动生成默认或复制构造函数。(只能使用在无参构造函数,和复制构造函数)
delete可以禁用指定的构造函数,以便禁止类使用过程中的相关操作。(无法作用在析构函数)
class A
{
string s;
A() = defalut; //默认合成的无参构造函数。
A(string _s); //有参构造函数
A(A &&a) = delete; //删除复制构造函数,因为string是一个引用类型,所以两个&&
}
四、类的组合
1、组合
类的组合描述的就是一个类内嵌其他类的对象作为成员的情况,他们之间的关系是一种保护与被包含的关系。
当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将被自动创建。所以在创建对象时既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化。
构造函数调用顺序:1、内嵌对象构造函数,如有多个按顺序。2、本类的构造函数。
析构函数的调用顺序与构造函数刚好相反。
#include <iostream>
using namespace std;
class B {
public:
B() {
cout << "B的构造函数" << endl;
}
~B() {
cout << "B的析构函数" << endl;
}
};
class C {
public:
C() {
cout << "C的构造函数" << endl;
}
~C() {
cout << "C的析构函数" << endl;
}
};
class A {
public:
C c;
B b;
A() {
cout << "A的构造函数" << endl;
}
~A() {
cout << "A的析构函数" << endl;
}
};
int main() {
A a;
return 0;
}
上面这个代码的执行顺序是:
2、前向引用声明
- 类应该先声明,后使用
- 如果需要在某个类的声明之前,引用该类,则应进行前向引用声明。
- 前向引用声明只为程序引入一个标识符,但具体声明在其它地方。
class B; //前向引用声明
class A
{ public:
void f(B b);
};
class B
{ public:
void g(A a);
};
使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,但是在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。请看下面的程序段:
class Fred; //前向引用声明
class Barney {
Fred x; //错误:类Fred的声明尚不完善
Fred* x; //正确,经过前向引用声明,可以声明Fred类的对象指针
};
class Fred {
Barney y;
};
应该记住:当你使用前向引用声明时,你只能使用被声明的符号,而不能涉及类的任何细节。
3、初始化类内的对象
如果类内的对象有默认构造函数,就会使用默认构造函数来初始化,但有时候没有默认构造函数或者说我们希望用有参构造函数来初始化类内的对象该怎么做?
class A{
public :
A(int m) :n(m){}; //表示用传进来的m值赋给n
private:
int n;
};
class B{
public :
B(int m) :a(m){}; //表示用传进来的m值作为参数来调用A的构造函数,初始化a
private:
A a;
};
五、结构体和联合体
1、结构体
结构体和类的唯一区别在于,结构体和类具有不同的默认访问控制属性:在类中,对于未指定访问控制属性的成员,其访问控制属性为私有类型;在结构体中,对于未指定访问控制属性的成员,其访问控制属性为公有类型;
struct 结构体名称
{
公有成员
protected:
保护型成员
private:
私有成员
};
2、联合体
联合体是一种特殊形态的类,可以有数据成员、函数成员、构造函数、析构函数,可以控制访问权限。默认访问控制属性也是公共类型的。
联合体的全部数据成员共享一组内存单元,因此一个联合体变量的成员同时至多只有一个是有意义的。
- 联合体的各个对象成员,不能用自定义的构造函数、自定义的析构函数和重载的赋值运算符。这些对象成员的对象成员也不能有。
- 联合体不能继承,也不支持多态。
union 名称{
public: //此行可不写,默认为public
公有成员
protected:
保护型成员
private:
私有成员
};
无名联合体
union {
int i;
float f;
};
i = 5; // 无名联合体可直接用变量名使用
f = 2.0; // 此时i赋值的内容已无效
六、枚举类型
枚举类型可以将一组整形常量组织在一起作为一个枚举类型.
两种枚举类型:不限定作用域的枚举类型、限定作用域的枚举类。
//不限定作用域枚举类型
enum (枚举类型名) {变量值列表};
//eg:
enum Weekday{SUN,MON,TUE,WED,THU,FRI,SAT};
//下面这个是未命名的、不限定作用域枚举类型。week1、week2都是该类型的变量
enum {SUN,MON,TUE,WED,THU,FRI,SAT} week1,week2;
- 枚举元素具有默认值,依次为0、1、2...
- 也可以声明时定义枚举元素的值,如下,然后后面的顺序依次加一,SAT为6.
enum Weekday{SUN = 7,MON = 1,TUE,WED,THU,FRI,SAT};
- 整数类不能直接赋值给枚举变量,枚举要将整数赋值给枚举变量,一个进行强制类型转换。
- 枚举元素是const类型,所以初始化枚举成员是提供的初始值必须是常量表达式,每个枚举元素本身就是一个常量表达式
枚举类
enum class 枚举类型名{变量值列表};
enum struct 枚举类型名{变量值列表};