类的定义
相关概念
- 抽象:对具体对象(问题)进行概括,抽出这一类对象的公共性质并加以描述的过程
- 数据抽象——数据成员
- 行为抽象——成员函数
- 封装:将抽象出的数据成员、行为成员相结合,将他们视为一个整体————类
- 继承与派生:保持原有类特性的基础上,进行更具体的说明
定义一个类
class 类名{ //类体
成员列表
}
类的特性
-
面向对象程序设计一般将数据隐藏起来,外部不能直接访问, 而把成员函数作为对外界的接口, 通过成员函数访问数据。即数据成员是属性,函数成员是方法,通过方法存取属性(将数据隐藏,成员函数标定了怎样访问是合法的)
-
C++规定,在局部作用域中声明的类,成员函数必须是函数定义形式,不能是原型声明。一般地,由于类是为整个程序服务的,因此很少有将类放到局部作用域中去定义
成员访问控制
- 对类的成员进行访问,来自两个访问源:类成员和类用户
- 类成员指类本身的成员函数
- 类用户指类外部的使用者,包括全局函数、另一个类的成员函数等
- 类的每个成员都有访问控制属性,由以下三种访问标号说明:public、private、protected
-
公有成员:用public标号声明,类成员和类用户都可以访问公有成员,任何一个来自类外部的类用户都必须通过共有成员来访问。显然,public实现了类的外部接口
-
私有成员:用private标号声明,只有类成员可以访问私有成员,类用户的访问是不允许的。显然,private实现了私有成员的隐蔽。
-
保护成员用protected标号声明,在不考虑集成的情况下,protected的性质和private的性质一致,但保护成员可以被派生类的类成员访问(专为继承设计)
-
类的成员函数
- 成员函数重载及默认参数。需要注意的是,声明成员函数的多个重载版本或指定成员函数的默认参数,只能在类内部中进行,默认参数仅指定一次(定义或声明)
类的数据成员
- 类的数据成员还可以是成员对象,即类类型或结构体类型的对象。若类A中嵌入了类B的对象,则称这个对象为子对象。除了定义数据成员和成员函数之外,类还可以定义自己的局部类型,并且使用类型别名来简化
- 成员函数代码只有共用的一段存储空间,调用不同对象的成员函数时都是执行同一段函数代码;而为每个对象的数据成员分配各自独立的存储空间
类的声明
- 类不能具有自身类型的数据成员。然而,只要类名一经出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用。
对象的产生
动态分配对象的一般形式为:
类名* 对象指针变量;
对象指针变量 = new 类名;
对象的赋值
- 如果一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值。这里所指的对象的“值”是指对象中所有数据成员的值
- 注意:
- 对象的赋值只对其中的非静态数据成员赋值,而不对成员函数赋值;
- 如果对象的数据成员中包含动态分配资源的指针,按照上述赋值的原理,赋值时只复制了指针值而没有复制指针所指向的内容。(浅复制);
成员初始化
- 两种情况
- 如果一个类中所有的数据成员是公有的,则可以在定义对象时对数据成员进行初始化,例如:
class Point {
public:
int x, y;
}
Point one = {10,10};//对象初始化
Point A[3] = {{10,10},{20,20},{30,30}};//对象数组初始化
- 如果类中的数据成员是私有的,如private的或protected的,就不能用这种方法初始化,因为外部不能直接访问私有的数据成员(python中的\__init__())
-
从初始化角度来看,可以认为构造函数分两个阶段执行
- 初始化阶段
- 普通计算阶段。初始化阶段由构造函数初始化列表组成,计算阶段由构造函数函数体的所有语句组成,初始化阶段先于普通的计算阶段。
-
构造函数初始化列表
- 有时必须用构造函数初始化列表。
如果没有为类类型的数据成员提供初始化列表,编译器会隐式地使用该成员的默认构造函数(没有形参的构造函数)。如果那个类没有默认构造函数,则编译器会报告错误。在这种情况下,为了初始化类类型的数据成员,必须提供初始化列表 - 一般地,没有默认构造函数的成员,以及const或引用类型的成员,都必须在默认构造函数初始化列表中进行初始化。
class point { private: int x, y; public: point(int i, int j){x = i, y = j;}//没有默认构造函数 void print(){ cout << x << '.' << y << endl;} }; class pointTest { private: point a; public: pointTest(int o, int j):a(i, j){ }//只能在初始化列表里对成员a初始化 };
- 有时必须用构造函数初始化列表。
-
三类构造函数
-
默认构造函数
- 默认构造函数由不带参数的构造函数,或者所有形参均是默认参数的构造函数定义。
- 合成默认构造函数:
- 任何一个类有且只有一个默认构造函数。如果定义的类中没有显式定义任何构造函数,编译器会自动为该类生成默认构造函数,称为合成默认构造函数(synthesized default constructor)
- 一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。换言之,如果为类定义了一个带参数的构造函数,还想要无参数的构造函数,就必须自己定义它
- 一般地,任何一个类都应该定义一个默认构造函数。因为在很多情况下,默认构造函数是由编译器隐式地调用的
-
转换构造函数
-
为了实现其他类型到类类型的隐式转换,需要定义合适的构造函数。可以用单个实参调用的构造函数(称为转换构造函数)定义从形参类型到该类类型的隐式转换。
-
例子:
#include <iostream> using namespace std; classData{ public: Data(const string& str = ""):s1(str){} void print(){cout << s1 << endl;} private: string s1; }; int main() { string i = "string"; Data a("world"); a = Data(i);//隐式转换 a.print();//输出string return 0; }
-
explicit关键字
- 可以禁止由构造函数定义的隐式转换,方法是通过将构造函数声明为explicit,来防止在需要隐式转换的上下文中使用构造函数
- C++ 关键字explicit用来修饰类的构造函数,指明该构造函数是显式的。explicit关键字只能用于类内部的构造函数声明上,在类定义外部不能重复它。
- 一般地,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。将构造函数设置为explicit可以避免错误,如果真需要转换,可以显示地构造对象
-
-
复制构造函数
-
复制构造函数又称为拷贝构造函数,它是一种特殊的构造函数。它的作用就是用一个已经生成的对象来初始化另一个同类的对象
-
合成复制构造函数
-
每个类必须有一个复制构造函数。如果类没有定义复制构造函数,编译器就会自动合成一个,成为合成 复制构造函数
-
与合成默认构造函数不同,即使定义了其他构造函数,编译器也会合成复制构造函数。
-
合成复制构造函数的操作是:执行逐个成员初始化,将新对象初始化为原对象的副本。(浅复制)
-
深复制举例:
#include <iostream> #include <string.h> using namespace std; class CA { public: CA(int b,char *cstr)//构造函数 { a = b; str = new char[b]; strcpy(str, cstr); } CA(const CA &C)//复制构造函数 { a = C.a; str = new char[a];//深复制,若为浅复制则写成str=C.str; if(str!=0) strcpy(ctr, C.str); } void show() { vout << str << endl; } ~CA()//析构函数 { delete str; } privatte: int a; char *str; }; int main() { CA a(10, "hello"); CA b=a; b.show(); return 0; }
-
-
以下三种情况会使用复制构造函数
-
用一个对象显式或隐式初始化另一个对象
-
函数参数按值传递对象时或函数返回对象时
-
根据元素初始化式列表初始化数组元素时
如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的大括号的数组初值列表形式来初始化数组时,则使用复制初始化来初始化每个元素。
总的来说,正是有了复制构造函数,函数才可以传递对象和返回对象,对象数组才能用初值列表的形式初始化。
-
-
-
对象的消亡
何时调用析构函数
- 对象在程序运行超出其作用域时自动撤销,撤销时自动调用该对象的系够函数。如函数中的非静态局部对象
- 如果用new运算动态地建立了一个对象,那么用delete运算释放该对象时,调用该对象的析构函数
合成析构函数
- 与复制构造函数不同,编译器总是会为类生成一个析构函数,称为合成析构函数。合成析构函数按照创建的逆序撤销每个非静态成员
- 合成析构函数并不删除指针成员所指向的对象,它需要程序员显式编写析构函数去处理
何时需要编写析构函数
- 如果类需要析构函数,则该类几乎必然需要定义自己的复制股早函数和复制运算符重载,这个规则称为析构函数三法则
对象的使用(用对象内的方法,操作对象内存有的数据)
对象数组
- 在建立对象数组时,需要调用构造函数。如果对象数组有100个元素,就需要调用100次构造函数
- 如果对象数组所属类有带参数的构造函数时,可用初始化列表按顺序调用构造函数,使用复制初始化来初始化每个数组元素。
Point P[3] = {Point(1, 2), Point(5, 6), Point(7, 8)} - 如果对象数组所属类有单个参数的构造函数时,定义数组时可以直接在初值列表中提供实参
Student S[5] = {20, 21, 19, 20, 19} - 对象数组创建时若没有初始化,则其所属类要么有合成默认构造函数(此时无其他的构造函数),要么定义无参数的构造函数或全部参数为默认参数的构造函数(此时编译器不再合成默认构造函数)
C++指针
CPP的指针被分成数据指针、函数指针、数据成员指针、成员函数指针四种且不能随便相互转换。其中前两种是c语言的,称为普通指针;后两种是c++专门为类扩展的,称为成员指针。
成员指针与类的类型和成员的类型相关,它只应用于类的非静态成员。由于静态类成员不是任何对象的组成部分,所以静态成员指针可用普通指针。
- 数据指针
- 函数指针
- 数据成员指针
-
数据成员类型 类名::*指针变量名 = 成员地址初值
class Data { public: typedef usigned int index;//类型成员 char get() const;//成员函数,使用const限定无法改变数据成员的数值 char get(index st, index eb) const;//成员函数 string content;//数据成员 index cursor, top, bottom;//数据成员 } string Data::*ps = &Data::content;
-
- 成员函数指针
- 定义成员函数的指针时必须确保在三个方面与它所指函数的类型相匹配:
- 函数形参的类型和数目,包括成员是否为const
- 返回类型
- 所属类的类型
- 一般形式为
- 返回类型 (类名::*指针变量名)(形式参数列表)= 成员地址初值:
- 返回类型 (类名::*指针变量名)(形式参数列表)const=成员地址初值
char (Data::*pmf)() const = &Data::get;
typedef char (Data::*GETFUNC)(Data::index, Data::index)const;
GETFUNC pfget = &Data::get;
- 定义成员函数的指针时必须确保在三个方面与它所指函数的类型相匹配:
- 使用类成员指针
- 通过 对象成员指针引用 (.*)可以从 类对象 或 引用 及 成员指针 间接访问类成员,或者通过 指针成员指针引用(->×)可以从 指向类对象的指针 及 成员指针 访问类成员
- 对象成员指针引用运算符左边的运算对象必须是类类型的对象,指针成员指针引用运算符左边的运算对象必须是类类型的指针,两个运算符的右边运算对象必须是成员指针
Data d, *p = &d;//指向对象d的指针 int Data::*pt = &Data::top;//pt为指向数据成员top的指针 int k = d.top; //对象成员引用,直接访问对象,直接访问成员,与下面等价 k = d.*top;//对象成员指针引用,直接访问对象,间接访问成员 k = p->top;//指针成员引用,间接访问对象,直接访问成员 k = p->*pt;//指针成员指针引用,简介访问对象,间接访问成员 char (Data::*pmf)(int, int) cconst;//pmf为成员函数指针 pmf = &Data::get;//指向有两个参数的get函数 char c1 = d.get(0, 0);//对象直接调用成员函数,与下面等价 char c2 = (d.*pmf)(0, 0);//对象通过成员函数指针简介调用成员函数 char c3 = (p->*pmf)(0, 0);//指针间接引用对象通过成员函数指针简介调用成员函数
- this指针
- 了静态成员函数外,每个成员函数都有一个额外的、隐含的形参this。在调用成员函数时,编译器向形参this传递调用成员函数的对象的地址
void Point::set(int a, int b) {x = a, y = b};//成员函数定义
编译器会改写为:
void Point::set(Point* const this, int a , int b)//const限定不允许改变指针指向 {this ->x = a, this -> y = b}
- 什么时候会用到this指针
- 在类的非静态成员函数中返回类对象本身的时候,直接使用return *this
- 当参数与数据成员名相同时,如this->n = n(能写出n=n)
- 了静态成员函数外,每个成员函数都有一个额外的、隐含的形参this。在调用成员函数时,编译器向形参this传递调用成员函数的对象的地址
对象的生命周期
- 局部对象
局部对象在运行函数时被创建,调用构造函数;当函数不运行结束时被释放,调用析构函数
-
静态局部对象
静态局部对象在程序执行函数第一次经过该对象的定义语句时被创建,调用构造函数。这总对象一旦被创建,在程序结束前都不会撤销。即使定义静态局部对象的函数结束时,静态局部对象也不会撤销。在该函数被多次调用的过程中,静态局部对象会持续存在并保存它的值
静态局部对象在程序运行结束时被释放,调用析构函数
-
全局对象
在程序开始运行时,main运行前创建对象,并调用构造函数;在程序运行结束时被释放,调用析构函数
-
自由存储对象
自由存储对象一经new运算创建,就会始终保持知道delete运算时,即使程序运行结束它也不会自动释放
const限定
-
对对象的限定
- 常对象
- 常对象的数据成员均是const的,因此必须要有初值。无论什么情况下,常对象中的数据成员都不能被修改
- 除了合成的默认构造函数和默认析构函数外,也++不能调++用常对象的非const型的成员函数
- 在实际编程中,有时一定要修改常对象中的某个数据成员的值,这时可以将数据成员声明为mutable(可变的)来修改它的值。声明形式为:
mutable 数据成员类型 数据成员名列表;//可变的数据成员声明
- 常数据成员
- 常数据成员只能通过构造函数初始化列表进行初始化
- 无论是成员函数还是非成员函数都不允许修改常数据成员的值
- 常成员函数
成员 非常成员函数 常成员函数 据成员 允许访问,可以修改 允许访问,不能修改 常数据成员 允许访问,不能修改 允许访问,不能修改 常对象数据成员 不允许访问和修改 允许访问,不能修改
常成员函数不能调用另一个非常成员函数
- 常对象
-
对指针的限定
- 指向对象的常指针
类名 *const 指针变量名 = 对象地址
指向不能改,能改所指向对象的值 - 指向常对象的指针
const 类名 *指针变量名;
指向能改,不能改所指向对象的值
- 指向对象的常指针
-
对引用的限定
同上
静态成员
- 通常,非静态数据成员存在于类类型的每个对象中,静态数据成员则独立于该类的任何对象,在所有对象之外单独开辟空间存储。在为对象所分配的空间中不包括静态数据成员所占的空间
- 访问静态成员时同样需要遵守公有及私有访问规则
- 静态数据成员必须在类外部定义一次(仅有一次),静态成员不能通过类构造函数进行初始化,而是在类外定义时进行初始化。
- 静态数据成员可以用作默认实参,非静态数据成员不能用做默认实参,因为它的值不能独立于所属的对象使用
class Data{ //Data类定义
Data& setbcolor(int a=bkcolor);
static const int bkcolor = 5;
}; - 静态成员函数与非静态成员函数的根本区别是:非静态成员函数有this指针,而静态成员函数没有this指针。因此,静态成员函数不能访问本类中的非静态成员。静态成员函数就是专门为了访问静态数据成员的。
- 静态成员函数不能被声明为const
友元
- 友元函数
- C++提供友元机制,允许一个类将其非公有成员的访问权授予指定的函数或类。友元的声明只能出现在类定义的内部的任何地方,由于友元不是授予友元关系的那个类的成员,所以它们不受访问控制的影响。通常,将友元声明放在类定义的开始或结尾
- 如果在一个类以外的某个地方定义了一个函数,在类定义中用friend对其进行声明,此函数就称为这个类的友元函数。友元函数可以访问这个类中的私有成员。
- 友元函数可以是另一个类的成员函数,称为友元成员函数。
- 友元类
- 不仅可以将一个函数声明为友元,还可以将一个类(如B)声明为另一个类(如A)的友元,这时类B就是类A的友元类。友元类B中的所有成员函数都是A类的友元函数,可以访问A类中的所有成员。
- 关于友元类的说明:
- 友元的关系是单向的而不是双向的。如果声明了类B是类A的友元类,不等于类A是类B的友元类。
- 友元的关系不能传递或继承。