声明:文中知识是《C++ primer 第五版(中文版)》、CSDN与我自己知识的一个汇总,欢迎指出文中的错误,欢迎指点。
概念
对象
一块能存储数据并具有某种类型的内存空间
类
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程以及设计技术。类的接口包括用户所能执行的操作;类的实现则包括的数据成员、负责接口实现的函数体以及定义类所需的各种死由函数。
封装实现了累的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
类想要实现数据抽象和封装,需要首先定义一个抽象数据类型。在抽象数据类型中,由类的设计者负责考虑的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。
特性
抽象
有以下几个层次的抽象:
1. 类本身是一种抽象数据类型,也就是说被我们视为一个整体,一个对象,一个值。
2. 类提供的接口也是一种抽象,用户程序员(区别于设计类的程序员,之后用户程序员统称用户)在使用类的时候只需要知道它做了什么,而不用关心它具体是如何做到的。
3. 继承与多态仍然是一种抽象——派生类的父类可以被视为一个高一层次的抽象;静态多态是一个函数层面的抽象;动态多态相当于把继承中的抽象运用起来。
4. 模板类可以视为一个类似于数据结构的抽象方式(比如我们实现栈的时候其实并不关心栈里的元素类型是int还是double或者是一个类)。
5. 在C++ primer中这样描述:
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类的区别,而以统一的方式使用它们的对象。
封装
- C++中class与struct的唯一区别在于默认的访问权限是private还是public,也就是说
class{
/* */
};
与
struct{
private:
/* */
};
等效;而
class{
public:
/* */
};
与
struct{
/* */
};
等效。
2. 封装有两个优点:1)确保用户代码不会无意间破坏封装对象的状态;2)被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
请看下方两个代码:
class student{
private:
char sex;//性别,仅应有'f'代表女性,'m'代表男性
/*…………*/
public:
void setsex(int i){
if(i%2){
sex='f';
}else{
sex='m';
}
}
/*………………*/
};
/*………………*/
int main()
{
student a;
a.setsex(255);
/*………………*/
return 0;
}
class student{
public:
char sex;//性别,仅应有'f'代表女性,'m'代表男性
/*…………*/
};
/*………………*/
int main()
{
student a;
a.sex='b';//此处的修改明显是错误的
/*………………*/
return 0;
}
可以发现,以上例子中优点1)是不言而名的,如果用户不是通过直接修改类内数据的方式完成对类对象的修改,那么修改是否合法合理仅取决于类的设计,只要类的设计良好,用户就不用担心会破坏封装对象的状态。对于优点2)来说,如果后续的版本认为要用’F’表示女性,对于后一个例子那么就需要把函数中每一处的a.sex=’f’改为a.sex=’F’,而前一个只需要把函数改了就好了。
P.S.尽管当类的定义发生改变时无需更改用户代码,但是使用了该类的源文件必须重新编译。
继承与多态
把后面的知识点看了应该就没问题了。
成员的访问控制
之前提到,面向对象编程的一个特征就是封装,而访问控制就是为了实现这个的。
public:定义在此说明符后的成员在整个程序内可访问。
private:定义在此说明符后的成员可以被类的成员、友元访问。
protected:定义在此说明符后的成员对于派生类的成员和有缘来说是可以访问的,同时派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,也就是只能访问派生类对象自己继承的基类成员,而不能通过派生类对象访问另一个基类对象的成员。具体见下:
class A{
protected:
int a;
};
class B:public A{
protected:
int b;
public:
friend void CorrectFun(B &b);
friend void WrongFun(A &a);
};
void CorrectFun(B &b){
b.b=0;
b.a=0;
}
void WrongFun(A &a){//此处导致编译失败
a.a=0;
}
每个类控制其成员对于派生类来说是否可访问,也就是说不是由派生类来控制,也就是说如下代码中,派生访问说明符对结果没有影响:
class Base{
public:
int pub_int;
void pub_fun();
protected:
int pro_int;
void pro_fun();
private:
int pri_int;
void pri_fun();
};
class Derive:public/protected/private Base{//不管这里的派生访问说明符是什么,不影响之后的结果
friend void Fun(Base &b);
};
void Fun(Base &b){
b.pub_int=0;//没问题
b.pro_int=0;//没问题,protected成员对于派生类的友元可见
b.pri_int=0;//编译出错,private成员对于派生类是不可见的
}
派生访问说明符的作用是控制用户通过派生类对于基类的访问权限,考虑下面的代码:
在有以下声明的情况下
class Base{
public:
int pub_int;
void pub_fun();
protected:
int pro_int;
void pro_fun();
private:
int pri_int;
void pri_fun();
};
class Pri_Derive:private Base{
};
class Pub_Derive:public Base{
};
int main()
{
Pri_Derive d1;
Pub_Derive d2;
d2.pub_fun();//没问题,pub_fun在派生类里是public的
d1.pub_fun();//编译出错,pub_fun在派生类里是private的
}
函数重载
几个函数名字相同但形参列表不同被称为重载函数,main函数不能重载。
顶层const与非顶层const的形参无法区分开(常量指针和普通指针无法区分)。
形参不同就是一种重载,但是在实际使用时却可能因为函数匹配过程中出现二义性编译失败,详情见函数重载
类相关知识点
拷贝控制(构造与析构)
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化对象的数据成员,无论何时只要类的对象呗创建,就会执行构造函数。
构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型;除此之外类似于其他的函数,构造函数也可能有一个(可能为空)的参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或者参数类型上有所区别。
不同于其他成员函数,构造函数不能被声明为const的。当我们创建类的一个const对象是,知道构造函数完成初始化过程,对象才能真正取得其”常量“属性。因此,构造函数在const对象的构造过程中可以向其写值。
未提供出事值得对象被执行默认初始化。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫默认构造函数默认构造函数无须任何实参。
只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。同时若一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数,并且此时不能再次定义无实参的默认构造函数(函数重载处细说)。
复制构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值。与默认构造函数不同,即使我们定义了其他的构造函数,编译器也会为我们合成一个复制构造函数。
当一个对象因为离开作用域/容器被销毁/想不开(?)等等原因被销毁时,首先调用其析构函数,然后按照声明成员顺序的逆序销毁成员。
友元
友元在类内部声明,仅置顶了访问的权限而并非通常意义上的函数声明,因此如要使用必须在有元声明外再专门对函数进行声明(尽管许多编译器并未强制要求友元函数必须在使用之前在类的外部声明)。
友元函数的访问权限是类本身的所有成员和继承父类的public/protected成员。
静态成员
通过在成员的声明前加上关键字static使其成为静态成员。
与其他成员函数一样,既可以在类内部也可以在类外部定义静态成员函数。
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建累的对象时被绑定的。这意味着它们不是有类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。
虽然通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内出事值,不过要求静态成员必须是字面值常量类型的常量表达式(简单理解为常量就行,考试应该不会考那么细)。
常对象
定义常对象必须提供初始值。并且类的常量对象、常量对象的引或指针都只能调用类的常函数。
只能通过构造函数初始化一个常数据成员。这里把初始化加粗是因为需要注意两种形式
class A{
private:
const int a;
public:
A(int g=0){a=g}
};
class A{
protected:
const int a;
public:
A(int g=0):a(g){}
};
不等价,前者无法通过编译。
通过在参数列表后面加const关键字声明,其作用是将*this指针转换成声明成指向常量的指针,所以在这样的常成员函数中不可以修改调用它的对象的内容。
虚基类
尽管在派生列表中同一基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个积累再一次间接继承该类。
在默认情况下,派生类中含有继承脸上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包括该类的多个子对象。这样的情况对某些要符合某些要求的泪来说显然是行不通的。
于是在C++中通过徐济成的机智解决上述问题。徐济成的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象成为虚基类。在这种机制下,不论虚基类在集成体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
我们置顶虚基类的方式是在派生列表中添加关键字virtual,这个关键字与派生访问说明符的顺序随意。
含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接积累在派生列表中出现的次序依次对其进行初始化。虚基类总是先于非虚基类构造,与它们在集成体系中的次序和位置无关。一个类可以有多个虚基类,此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。
如C++ primer中的例子:
在有如下声明的情况下
class ZonnAnimal{/*...*/};
class Bear:virtual public ZooAnimal{/*...*/};
class Character{/*...*/};
class BookCharacter:public Character{/*...*/};
class ToyAnimal{/*...*/};
class TeddyBear:public BookCharacter,public Bear,
public virtual ToyAnimal{/*...*/};
创建一个TeddyBear对象需要按照如下次序调用这些构造函数:
ZooAnimal(); //Bear的虚基类
ToyAnimal(); //直接虚基类
Character(); //第一个非虚基类的间接积累
BookCharacter(); //第一个直接非虚基类
Bear(); //第二个直接非虚基类
TeddyBear(); //最低层的派生类
同时正如以往所学,对象的销毁顺序与构造顺序正好相反。
动态多态(动态绑定)
在C++语言中,当我们使用基类的引用或(指针)调用一个虚函数时将发生动态绑定。
这部分光靠说我实在没法解释完,就自己看下《C++ primer第五版(中文版)》p526-542吧。
类模板
这部分个人感觉比较偏向于实际应用,没什么好说的(或者说要说的话深入了一些,我觉得考试就没必要细究那么多了),可以尝试一下把之前数据机构的实验里面的类变成模板类。