类与对象
类的定义
- 类支持用户自定义数据类型,使用关键字class,类体用{}界定,以;结束
class a { public: int b; a(int b, char c, long d); protected: char c; private: long d; }
- 类的成员包括函数成员和数据成员。定义类的目的是对某种类型的实体进行处理
- 在类的定义中,实体的属性是数据形式,即类的数据成员;实体的行为、功能或者对类中的数据成员进行操作称作方法,被表示为函数
- 类的数据成员在定义类体中,形式为:数据类型 成员名;
-
类的函数成员有两种定义方法:
- 现在类体中进行函数声明,在类体外进行函数实现。在类的外部定义成员函数时必须 在成员函数名前给出所属类名,并用::域标识符连接。如:
class a { void b(); }; void a::b() { .... };
直接在类体中完成函数体的实现
- 通常数据成员放在私有部分中,公有部分存放方法
- 同类对象可赋值,将所有数据覆写
类与对象的关系
- 在C++中,一个”类“被视作一个用户自定义的数据类型,可以在其上衍生出本类型的变量。”类“的数据类型的变量被称作”对象“,与结构类似
- 对象的定义方式为:类名 对象
- 如果对象实质是外部使用对象的成员,格式为:对象.成员名
- 使用对象的实质是通过调用对象的成员函数来完成程序功能,这种调用属于外部访问,收到访问限定符的约束,只能使用public成员
- 调用类中的方法时,方法将使用相应类对象中相应成员的值
构造函数
- 构造函数为在声明时构造对象的函数
- 函数名要与类名相同
- 函数的参数不能与类中的对象同名
- 构造函数声明在公共区域内
- 构造函数本质上为创建一个临时对象,赋值后销毁
-
使用构造函数有两种方法:
- 显式使用:
classname a=course("123",4);
- 隐式使用:
classname a("123",4);
- 使用动态内存分配时亦会调用构造函数
classname *p = new classname//调用默认构造函数 calssname *q = new classname(...)
在此情况下,该对象没有名称,通过指针管理该对象
- 只能在对象声明时使用构造函数,不能通过对象调用构造函数
- 构造函数可以没有返回值和返回类型
- 构造函数可重复调用
- 当声明对象为const类型时,不能直接调用对象里的非const方法,因为不能保证数据不被修改。解决方法为在将const关键字放置在函数定义形参括号后面,意为保证函数不会修改调用对象。只要类方法不修改调用对象,就要将其声明为const
- 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值
默认构造函数
- 类的对象由构造函数创建。当且仅当未定义构造函数声明类对象时,C++将自动调用默认构造函数创建空的对象。(类似于int a;)
- 定义构造函数后,默认构造函数无效,必须自行定义默认构造函数,否则无法创建对象(如无法直接使用classname a;)
- 定义默认构造函数的方式有两种:
- 直接给已有的构造函数所有参数提供默认值
- 通过函数的重载,定义一个没有参数版本的构造函数
- 在设计类时,通常应提供对所有类成员作隐式初始化的默认构造函数
复制构造函数/类和动态内存分配
- 在类的构造函数中使用动态内存分配时要注意
- 如果在构造函数中使用new初始化指针成员,在析构函数中应使用delete
- new对应delete,new[]对应delete[]
- 如果有多个构造函数,则必须以相同的方式使用new,要么都在带中括号,要么都不带,因为析构函数只能有一个版本。
-
在处理形如classname b;classname a = b时,系统将创建一个classname类型的临时变量并为其赋给b的值->将临时变量的值赋给a->调用析构函数销毁临时变量。(成员复制->浅复制)。在没有定义复制构造函数的情况下,编译器将自动创建一个复制构造函数,进行浅复制。此时如果类中包含动态内存分配的话
classname(const classname& b) { //classname中包含了使用new分配的字符串t ... this->t = b.t; }
其中临时变量指向的地址与b中指向的地址相同,调用析构函数时会将临时变量中的指针delete,也就破坏了b中原本的数据。
- 此时须定义一个复制构造函数,接收相同类对象的引用作为参数。
- 执行test a = b的步骤为:调用复制构造函数->创建test对象b的副本->将副本赋值给a->调用析构函数删除副本。(副本复制->深复制)此时副本的信息与b相同,但地址不同。调用析构函数不会造成b的数据损坏
classname(const classname& b) { //classname中包含了使用new分配的字符串t ... int length = strlen(b.t); this->t = new char[length]; strcpy(this->t, b.t); }
- 使用对象作为形参的函数,在调用时会为实参创建临时变量,函数执行完后销毁。对象引用作为形参时函数不会创建临时变量。
成员初始化列表
-
如果class是一个类,men1,men2,men3都是这个类的成员的成员,则类构造函数可以使用如下的语法来初始化数据成员:
class(typename m,typename n):data1(n),data2(0),data(m+2) { .......... }
- 初始化工作在对象创建时完成,此时还未执行括号中的代码
- 成员初始化列表只能用于构造函数
- 列表初始化的顺序只与变量在类内声明的顺序有关,与初始化写的顺序无关
- 此时接收的参数为实参而非副本
- 必须使用这种格式初始化类内非静态const成员
- 必须使用该格式初始化引用成员
类内初始化(C++ 11)
class test
{
int a = 5;
...
}
- 这与使用成员初始化列表等价
- 使用成员初始化列表的构造函数将覆盖相应的类内初始化
析构函数
- 析构函数是用于删除使用完毕对象的函数
- 析构函数的函数名~+类名,如 ~course()
- 析构函数和构造函数一样,不需要返回值和声明类型。
- 析构函数没有参数
- 通常情况下,由编译器决定何时调用析构函数。
- 如果创建的是静态的存储类对象,析构函数在程序结束时调用
- 如果创建的是动态的存储类对象,析构函数在使用delete指令时调用
- 如果创建的是自动的存储类对象,析构函数在离开作用域时调用
- 通常不应该在代码中显式地调用析构函数
运算符重载
- 运算符重载为对用户定义的类型定义相对应的方法。
- 运算符重载的定义方式为:返回类型 operator 运算符(参数) {.....},如:
class T { public: int c; int operator+ (int b){return c+b;} }
此时可显式调用
T a.operator+(5);
也可以隐式调用
int d = T a+5;
- 注意事项
- 重载的运算符必须是有效的C++运算符
- 在运算符表示法中,运算符左侧的对象为调用对象,右侧的为被调用对象
- 重载的运算符不一定是成员函数,但至少有一个操作数是用户自定义类型
- 使用运算符时不能违反原来的句法规则,如不能将%重载成只使用一个操作数,重载不改变运算符的优先级
- 不能创建新的运算符,如不能创建operator**()表示求幂
- 实现特殊的工作,如交换两个对象的成员值,应定义新的方法而不是重载运算符
- 不可重载的运算符
- sizeof
- 成员运算符(.)
- 成员指针运算符(.*)
- 作用域解析运算符(::)
- 三目运算符
- typeid:RTTI运算符
- const
- dynamic :强制类型转换运算符
- reinterpret :强制类型转换运算符
- static
友元
- 友元不是类的成员,但与类成员函数具有相同的访问权限。友元分为友元函数、友元类、友元成员函数。
- 在为类重载二元运算符时,通常在类方法中定义重载,因此运算符左侧必须是调用对象。(a+7)在交换调用对象与非调用对象的位置时(7+a),需要用到友元函数
- 友元函数的声明方式为
- 在类公共空间声明函数,在函数返回类型前加上friend关键字
- 在类外定义函数。定义函数时不需要使用关键字friend
class test { public: friend int operator+ (int a, test& b); int operator+ (int a); ..... } int operator+ (int a, test& b) { ... }
- 亦可以通过重载修改传入参数顺序,使用非友元函数
- 在类中调用其他类处理时(如调用ostream)需要将其声明为友元函数,否则可能引发调用参数过多的问题
- 类可以成为另一个类的友元,此时类中的方法可以访问友元类的数据:friend class a
- 使用前向声明将类成员函数声明为另一个类的友元
class TV;//前向声明 class remote { friend class TV; } class TV//后定义 { friend remote::...();//友元函数 }
- 使用前向声明时,使用类名之前要先声明类。声明友元后应先定义类再定义友元函数
类的自动类型转换与强制转换
-
只接受一个参数的构造函数(可以有多个参数但只有一个为非默认参数)可以将相应参数类型自动转换为类对象,相当于隐式调用了构造函数
class test { public: double a; test(double b = 5):a(b); }
在以下情况编译器会做此类型转换
• 将test对象初始化为double值时————test a = 7.5
• 将double值赋给test对象时——————test a; a=7.5
• 将double值传递给接收test参数的函数时
• 返回值声明为test类的函数试图返回double值时
• 在上述任意一种情况下,使用可转换为double值的任意类型时(int,char....) - 当且仅当转换不存在二义性时才会进行二步转换(int->double->test类),如构造函数同时有double和long类型的版本时,int类型的转换具有二义性(double/long),编译器将报错
-
在构造函数前加上explicit前缀可以禁止编译器进行自动类型转换,只能进行显式的转换
double a = (double)5;
转换函数
- 将某种类转换为某种类型需用到类的转换函数
使用格式为operator typename(),返回值为类型为typenameclass { operator int(); }
- 注意:
- 转换函数必须是类方法
- 转换函数必须有返回值
- 转换函数不能指定返回类型
- 转换函数不能有参数
- 当类定义了多种转换方式时,使用显式强制类型转换避免二义性错误
- 转换函数可以使用explicit前缀来禁止自动转换,强制显式转换
- 为两个类对象重载运算符时,使用接收两个类型引用为参数的友元函数;
- 重置类对象与内置类型的运算时,将运算符显式重载为接收该类型为参数的函数
指向对象的指针
- 使用方式:Typename *point = new typename(参数)或Typename *point = Typename变量
- 与结构体指针类似,解引用后可直接使用句点运算符访问对象,或使用间接成员运算符
- 指针为自动变量对象时,调用构造函数;对象离开作用域时调用析构函数
- 指针指向new创造对象时,仅当显式地调用delete时才调用析构函数
- new创建对象时,为对象分配内存,即为对象内非静态成员分配内存(同类型对象共享静态成员)
- 定位new运算符为对象分配指定长度和位置的空间,但使用定位new运算符创建的对象不能使用delete删除,必须保证显式地调用析构函数
- 注意使用定位new运算符创建的对象,删除的顺序与创建的顺序相反,因为晚创建的对象可能依赖于先前创建的对象(虚基类)。
类继承
- 类继承提供可重用的代码,在已有的类上添加功能、数据或方法。
- 类继承的语法为calss a:public/private/protect class b。此时原始类(b)成为基类,a为b的派生类,public等为继承方式
- 派生类含有基类的所有数据内容和方法,但无法直接访问基类的private类型的数据,需要使用基类的方法来访问。类外变量无法直接访问protect类型的数据,但派生类可以像public类型一样直接访问基类的protect类型数据。
- 派生类的友元函数不是基类的友元函数,但可通过强制类型转换成基类来调用基类的友元函数
- 派生类需要自身的构造函数,派生类的构造函数需要通过成员列表初始化调用基类的构造函数
- 创建派生类的过程为:调用基类构造函数->调用派生类构造函数,即先创建基类,在基类的基础上创建派生类
- 如果派生类构造函数不调用基类的构造函数,则将自动调用基类默认构造函数创建派生类
- 销毁派生类时,先调用派生类的析构函数,再调用基类的构造函数
特征\继承方式 | 公有继承 | 私有继承 | 保护继承 |
---|---|---|---|
公有成员变成派生类的 | 公有成员 | 私有成员 | 保护成员 |
私有成员变成派生类的 | 私有成员 | 只能通过基类接口访问 | 保护成员 |
保护成员变成派生类的 | 保护成员 | 私有成员 | 保护成员 |
能否隐式向上转换 | 能(派生链) | 能(只能在派生类中) | 不能,必须显式转换 |
虚函数
- 在类方法前加上virtual前缀可以将类方法声明为虚方法
class a
{
virtual a& tt()
{
......
}
}
class b : public a
{
virtual b& tt()
{
.......
}
}
- 方法通过指针或引用调用时,默认情况下程序将根据指针或引用的类型决定调用的方法。虚方法将根据引用或指针所指向的类型调用相应的方法。
a* test[2] = {&(a t), &(b q)}; test[0]->tt()//调用a::tt() test[1]->tt()//调用b::tt()
- 基类的析构函数应声明为虚方法,确保按正确的顺序调用析构函数
- 友元不能是虚函数,因为友元不是类成员。可以通过让友元使用虚函数成员函数替代
- 如果派生类没有定义函数,将使用该函数的基类版本。如果派生类位于派生链中,将使用最新的函数版本
- 注意,在派生类中重新定义函数(如基类函数接收一个int类型作为参数,派生类中的同名方法不接受参数)不会生成函数的两个重载版本,而是隐藏基类中的函数版本。
- 如果重新定义继承的方法,应确保函数原型与基类的原型完全相同。可以通过返回类型协变修改同名函数的返回值类型。注意返回类型协变只适用于类方法的返回值而非参数
-
如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则其余的重载版本将被隐藏。
class a { virtual a& tt() { ...... } virtual a& tt(int g) { ....... } } class b : public a { virtual b& tt() { ....... } virtual b& tt(int g) { ....... } }
- 注意,如果不需修改,则新定义可只调用基类版本。
virtual b& tt(){a::tt();}
纯虚函数
- 纯虚函数提供未实现的函数,纯虚函数声明的结尾处为=0
class a
{
virtual a& tt()=0;
}
- 纯虚函数可以没有定义
- 当类声明中包含纯虚函数时,则不能创建该类对象
抽象基类
- 从多种不同的类中抽象出共性,将共性放至抽象基类中,然后从抽象基类派生出不同的类型,派生出的类型称为具体类
- 抽象基类使用纯虚函数实现多态,在函数原型中使用=0指出类是一个抽象基类,但在抽象基类中可以不定义该函数。
派生链的指针关系
- 基类的引用和指针可以指向派生类,但只能调用基类的成员。派生类到基类转换称为向上强制转换
- 派生类的引用和指针不能指向基类。基类到派生类的转换成为向下强制转换。只能显式地使用向下强制转换。
- 基类的指针数组可同时存放基类和派生类的内容,通过调用各元素的虚方法实现多态
保护继承
- 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。
- 使用保护继承时,派生链的类对象都可以调用基类的接口
- 保护继承只能在派生类中强制向上转换(非派生链)
类包含
- 类包含通常用于建立has-a关系的组合,如“学生”类包含成绩和姓名,即可使用下例代码
class student { private: string name; vector<double> scores; ...... public: friend ostream& operator<<(ostream& a, student target); }
- 在上述示例中,student类可以对name使用string的方法,对scores使用vector类的方法(类包含获得了string和vector的实现),但是student不能像name一样使用string类的+=()方法(类包含不能获得接口)
- name和scores可以看作是student类的成员,在进行列表初始化时使用成员名而不是类名 studeng():name(" "),scores(){};
-
被包含对象的接口不是公有的,但可以通过对象调用,如
ostram& operator<<(ostream& a, student target) { a<<target.name<<a.name.size()<<endl; }
target.name是一个string对象,
a<<target.name
会自动调用string类的operator<<(ostream&,const string&)
私有继承
class test:private string,private vector<double>
{
test():string(" "),vector<double>(){};
int size() {return string::size();}
const string& name() {return (const string&) *this;}
ostream& operator<<(ostream&a const test & bb)
{
a<<(const string& bb)<<endl;
}
ostream& operator<<(ostream&a const test & bb)
{
a<<(const vector<double>& bb)<<endl;
}
}
- 私有继承与类包含一样,只继承接口不继承实现。
- 私有派生类只能通过显式强制类型转换变成基类,不能自动转换。
- 私有继承使用private限定符
- 私有继承使用类名标识构造函数
- 私有继承使用类名和解析运算符调用基类的方法
- 私有继承使用强制类型转换来访问基类对象
- 示例使用强制类型转换来调用test类中的string对象
- 私有继承通过强制类型转换调用基类的友元函数:
- 注意,通过转换为不同的类型来匹配不同基类的友元函数
- 类包含与私有继承的区别:
- 类包含可以在一个类中包含同类的多个子对象,私有继承派生类中只拥有一个只能通过强制类型转换访问的基类匿名对象
- 类包含不能访问被包含类的保护成员,但私有继承可以访问
- 私有继承可以通过虚函数实现多态
多重继承
- 多重继承指继承多个基类的继承方法,使用虚基类来避免在使用多态时的诸多问题
class a:public b, private c
虚基类
- 虚基类使得从多个同基类的类派生出的对象只继承一个基类对象
-
虚基类声明关键字为virtual
class c; class b : public c; class d : public c; class a : virtual public b, protected c vitrual public d { a(..):b(..),c(..),d(..); }
- 使用虚基类时,需要显式地调用所需基类的构造函数
- 对于单继承,如果没有重新定义方法,则将使用最近祖先的定义。对于多重继承,如果每个祖先都有同名的方法,则会造成二义性。此时需使用域解析运算符来指出需要使用哪个方法
- 基类和虚基类混合继承时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象
- 使用虚基类时,通过优先规则解决名称二义性。派生类中的名称优先于直接或简介祖先类的相同名称
类模版
- 类模版常用于实现泛型编程,在随程序运行的过程中动态地修改实现,通常用在容器类中
- 模版可用作结构、类或类模版的成员
- 采用模版定义时,在类声明前加上template,尖括号内的内容相当于模版类所需的类型名
template<typename T>
class stack
{
...
}
- 此时可以用T做模版类型,相应地,类名限定符发生改变(增加了模版类型参数)
stack<typename>()
- 注意,如果在类内定义函数则可以省去模版声明和限定符(同类参数传入仍需要注意类名带模版参数)
- 模版接收不同的参数(如数组大小)时只生成一个类声明,并将参数传递给类的构造函数
-
模版可以被递归调用
stack<stack<int> >()
此时的模版元素为自定义的stack类- 再如:
stack< stack<char*>(2) >c(3)
c含有三个元素,每个元素为含有两个元素的stack类,可直接c[2][1]调用
注意,在模版调用中,维的顺序与等价的二维数组相反
stack <stack<int>(2) >(3)=int[3][2]
- 再如:
- 模版可以使用多个模版类型参数
- 模版可接受指定的特殊类型作为参数,如
template<typename T, int n> ,int n
称为非类型或表达式参数。- 表达式参数有一些限制。表达式参数可以是整型(非浮点)、枚举、引用或指针
- 模版代码不能修改参数的值(n=5、n++)×,也不能引用参数的地址(&n、使用指向n的指针)×
- 含有表达式参数的模版,接收不同的参数时会生成不同的声明(a与a<double, 6>会生成两个独立的类声明)
模版的具体化
- 隐式具体化:
stack<int> c(3)
为隐式具体化,即它们声明一个或多个对象并指出所需类型,而编译器使用通用的模版提供的方法生成类的定义- 编译器在需要对象之前不会生成类的隐式实例化:
stack<int>*p(3)
(单纯的指针声明,不需要对象)p = new stack(3)
(指针赋地址,需要对象)
- 显式实例化
- 当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化, 显式实例化声明必须位于模版定义所在的名称空间中(通常位于头文件),如下面的声明
template class stack<int>
虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模版来生成具体化。
- 当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化, 显式实例化声明必须位于模版定义所在的名称空间中(通常位于头文件),如下面的声明
- 显式具体化
- 显式具体化是对特定类型使用的特殊定义
- 显式具体化的使用方式为
template<>class classname<type>{....}
- 如:
template<> class stack<int>{新的定义.....}
当请求int类型的stack模版时,将使用显式具体化的定义而非模版通用定义
-
部分具体化
- 部分具体化可以给类型参数之一指定具体类型。
- 方法一:对类型具体化,如:
template<classname T1> class stack<T1, int>{新的定义.....}
- 方法二:对传入参数具体化
template<classname T1, classname T2>class stack<T1,T1,T2>{新的定义....}
- 方法三:对类型指针具体化
template<classname T> class stack{.....}; template<classname T*> class stack{新的定义.....}
模版类和友元
模版类的非模版友元函数
- 在类模版中将一个常规函数声明为友元,该函数将成为模版所有实例化的友元。
template<typename T> class test { private: .... public: friend void print(....); }
- print()可以通过访问全局对象、使用全局指针访问非全局对象、创建对象、访问静态类数据成员来访问test对象
-
为友元函数提供模版类的参数,必须指定特定的具体化或模版
friend void print(test<T>);
也就是说,带test参数的print将成为test类的友元,带test参数的print将是test类的友元(类似于函数重载)。在定义时,必须要对友元的参数具体化,如:
void print(test<int>){....} void print(test<double>){.....}
模版类的非约束模版友元函数
- 通过在类内声明模版,可以创建非约束友元函数,即每个函数的具体化是每个类具体化的友元,友元模版类型参数与模版类类型参数不同
template<typename T> class test { private: .... public: template<typename G> friend void print(test<G>&) {....}; template<typename G> friend void tt(test<G>&){...}; }
print、test是所有test类型具体化的友元,能访问所有具体化的test成员
模版类的约束模版友元函数
- 友元函数本身可以成为模版,使类的每一个具体化都获得与友元匹配的具体化(不需对友元参数具体化)
- 步骤为
- 在类定义前声明模版函数
template<typename T> void print(T&) ; template<typename T> void tt<T>();
- 在类中将模版声明为友元
template<typename T> class test { private: .... public: friend void print<>{....}; friend void tt<T>{...}; }
- 声明中的<>指出这是模版具体化,对于print(),<>可以为空,因为其可以从函数参数(TT&)推断出具体化类型。tt()没有参数,因此必须指定类型。
- 步骤为
模版别名(C++ 11)
- 使用typedef为类型指定别名简化代码:
typedef std::vector<int>(12) veci
-
使用模版别名简化代码
template<typename T> using arrtype = std::array<T,12>
此时arrtype是一个模版别名,使用时需指定类型
即arrtype<T>表示类型arrtype<T,12>
- using= 可以用于非模版,效果等同于typedef
using veci = std::vector<int>(12)
嵌套类
- 可以将类声明放在另一个类中,在另一个类中声明的类成为嵌套类。
class a
{
public:
class b
{
public:
....
}
b c;
...
}
- 嵌套类的外部访问权限与类成员相同,且必须使用作用解析运算符来访问嵌套类成员,如
a::b.()
- 初始化包含对象需要使用列表初始化
a(..) : b(..)
文章评论