C++ 类
基本语法
基本形式
|
|
大括号内为类体,装成员函数、数据成员
例
|
|
定义
类成员
类可以没有成员,也可以定义多个成员。成员可以是数据、函数或类型别名。所有的成员都必须在类的内部声明
数据成员
变量、数组、常量、对象、对象数组
对象称为组合成员
成员函数
成员函数必须在类内部声明,可以在类内部定义,也可以在类外部定义。如果在类内部定义,就默认是内联函数
分类:
- 特殊函数
构造函数(浅/深拷贝构造函数)、析构函数、常函数/只读函数、静态函数、虚函数、纯虚函数、赋值函数
- 一般函数
对类的实例化
创建对象
1 2 3 4
Test test1, test2(3), test3(3, 4); Test tArray[10]; Test *tPtr = &test1; Test &tRef = test1;
动态地为其分配内存
1 2 3 4 5 6 7 8
Test *p; p = new Test; delete p; //对象数组 p = new Test[5]; delete[] p; //堆对象调用函数 p->Show();
定义函数体
|
|
调用类的成员
- 一般调用
|
|
- 引用调用
|
|
- 开辟内存调用
|
|
- 使用无名对象调用(语句结束,自动析构)
|
|
- 指针调用
|
|
this指针常量
成员函数(除了静态函数)具有一个附加的隐含形参(表现为Test *const this
),即 this 指针,它由编译器隐含地定义。成员函数的函数体可以显式使用 this 指针
常函数
特点
可以使用数据成员,不能进行修改,对函数的功能有更明确的限定;
常对象只能调用常函数,不能调用普通函数;
常函数的 this 指针是 const CStu*,即指向常量的常量指针(常量指针常量)
格式
void fun() const {}
例
int Geta() const { return a; }
构造函数
特征
- 函数名与类名相同
- 一般有形参表
- 构造函数是一种特殊的成员函数,不需要人为调用,而是在对象建立的时候自动被执行
例
|
|
Test ()
为无参构造函数或默认构造函数,写这个函数的好处是当你在创建对象的时候并不想立即对它初始化,而是在后续的工作中再进行赋初值,即:Test test1;
部分 IDE 会自动生成一个默认构造函数
Test(int x,int y)
完成了初始化工作,它有两个形参,分别给数据成员 a,b 进行初始化,定义对象的时候传入了 3 和 4,则 a 和 b 被初始化为 3 和 4成员初始化表
用来赋初值,可在构造函数的形参中赋初值
在类内
1
Test (int x,int y):a(x),b(y) {}
在类外
1 2 3 4
Test::Test(int x,int y):a(x),b(y) { //… }
在含有组合成员的类里,构造函数、拷贝构造函数都需要用成员初始化表
构造函数可以重载,也可以带默认参数
1 2
Test (int x = 0,int y = 0):a(x),b(y) {} Test test(3);
一旦指定了 x = 0,就必须指定 y 的值
所以这样是错误的:
Test (int x = 0,int y):a(x),b(y) {}
在类内,默认值写在()中
在类外,~~默认值写在{}中,~~声明写,定义不写
分类
默认构造函数
无实参
Point() {}
一般构造函数
Point(int x = 0, int y = 0) : xPos(x), yPos(y) {}
拷贝构造函数
Point(const Point &N) {}
用一个已经生成的对象来初始化另一个同类的对象
可以用成员初始化表
格式
1 2 3 4
类名 (const 类名& obj) { //函数体 }
例
1
Test (const Test& t):a(t.a),b(t.b) {}
1 2 3 4 5
Test(const Test &t) { this->a = t.a; this->b = t.b; }
如果不定义复制构造函数,以上对象也可以这样进行初始化,原因就是系统也会自己生成一个复制构造函数
在主函数内调用
1 2 3
Point p1(3, 4); Point p2 = p1; Point p2(p1);
转换构造函数
带一个实参的构造函数,它可以实现数据量类型的隐式转换和强制转换
|
|
可以用成员初始化表
深拷贝构造函数
浅拷贝
默认拷贝构造函数可以完成对象的数据成员简单的复制,这也称为浅拷贝
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间
深拷贝
深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝
经深拷贝后的指针是指向两个不同地址的指针
例
只占有栈空间的类,可以用默认的浅拷贝函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Test1 { private: int p; public: Test(int x) { this->p = x; } //浅拷贝 Test(const Test1 &a) { this->p = a.p; } };
占有堆空间(指针)的类,使用深拷贝函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Test2 { private: int *p; public: Test2(int x) { this->p = new int(x); } //深拷贝 Test2(const Test2 &a) { this->p = new int(*a.p); } };
总而言之,浅拷贝会把指针变量的地址复制; 深拷贝会重新开辟内存空间
析构函数
作用
析构函数在类里起了一个“清理”的作用,比如类中有需要动态开辟内存的成员,而在程序结束之后我们需要释放内存,这时只要将释放内存的语句写在析构函数中,而系统在程序运行结束之后会自动执行析构函数,进行内存的释放以及对象的销毁
先构造的后析构
格式
默认的析构函数
~Test(){};
占用堆空间的类的析构函数
1 2 3 4 5
~Test() { if (p != NULL) delete p; }
赋值运算符函数
格式
类名 &operator=(const 类名 &source_arg)
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
class Test { private: int *p; public: Test(int x) { this->p = new int(x); } void Show() const { cout << *p << endl; } Test(const Test &a) { this->p = new int(*a.p); } ~Test() { if (p != NULL) delete p; } Test &operator=(const Test &a) { if (&a != this) //防止自我赋值而丢失资源 { if (p != NULL) delete p; //主动释放原资源 this->p = new int(*a.p); //申请资源 } return *this; //返回赋值结果 } };
要点
如果对象在声明的同时马上进行初始化操作,则称之为拷贝运算
如果对象在声明之后,再进行赋值运算,称之为赋值运算
在类外定义
Test &Test::operator=(const Test &N)
类模板
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
template <class T1, class T2, class T3> class Student { public: Student(T1 name, T2 age, T3 score) { //......... } T1 m_Name; T2 m_Age; T3 m_Score; }; //主函数中声明对象 Student<string,int,float>s("Tom",18,85.5);
类的函数模板
如果在类外定义,类外要写上函数模板的形式,类内声明时不用
1 2
template <class T1, class T2> Student<T1, T2>::Student(T1 name, T2 age) : m_name(mName), m_age(mAge) {}
大大大大大例子
|
|
静态成员
静态数据成员
定义
在一个类中,如果将一个数据成员申明为 static,这种成员就被称为静态数据成员。与一般数据成员不同的是,无论建立多少个类的对象,都只有一个静态数据成员拷贝
创建、定义及初始化
格式
创建
static 数据类型 变量名
定义及初始化
(Type className::VarName = value)
数据类型 类名::静态数据成员名 = value;
数据类型 类名::静态数据成员名(value);
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class Test { private: int m_num; static int m_count; //创建静态数据成员 public: Test(int num = 0) { m_num = num; m_count++; } }; int Test::m_count = 0; //定义及初始化静态数据成员,类的静态成员变量需要在类外分配内存空间 int Test::m_count(0); //第二种方法
访问
格式
- 对象名.静态数据成员名
- 类名::静态数据成员名
例
1 2 3 4
Test t1(1); int a; a = Test::m_count; //公有的静态成员才能这样被访问 a = t1.m_count;
说明
静态数据成员的生命期不依赖于任何对象,为程序的生命周期
静态数据成员需要在类外单独分配空间,
静态数据成员在程序内部位于全局数据区
静态数据成员属于类,而不像普通的数据成员那样属于某个对象,因此我们可以用“类名::”这样的形式访问静态数据成员
静态数据成员在该类的任何对象创建之前就已经存在。因此,公有的静态数据成员可以在对象定义之前就被访问
静态成员函数
格式
static 返回类型 静态成员函数名(形参列表);
例
1 2 3 4
static int getCount() { return m_count; }
调用
- 格式
- 对象名.静态成员函数名
- 类名::静态成员函数名
- 例
1 2
Test::getCount; t1.getCount;
- 格式
说明
静态成员函数属于整个类所有,没有 this 指针
静态成员函数只能直接访问静态成员变量和静态成员函数
静态成员函数可以定义成内嵌的,也可以在类外定义,在类外定义时,前面不需要加 static
使用静态成员函数的一个原因就是可以用它在建立任何对象之前处理静态数据成员
译系统将静态成员函数限定为内部连接,也就是说,与现行的文件相连接的文件中的同名函数不会与该函数发生冲突,维护了该函数的安全性,这是使用静态成员函数的另外一个原因
友元
概念
C++提供了友元机制,允许一个类将其非公有成员的访问权限授予指定的函数或类。友元的声明只能在类定义的内部,因此,访问类非公有成员除了自身成员,还有友元
友元的作用在于提高程式的运行效率,但是,他破坏了类的封装性和隐藏性,使得非成员函数能够访问类的私有成员
友元函数
格式
在类中使用 friend 关键字来添加友元函数
friend 类型 函数名(形式参数);
例
1 2 3 4 5 6 7 8
//类内 friend int getSumNum(Test t1,Test t2); //类外 int getSumNum(Test t1,Test t2) { int result=t1.m_num+t2.m_num; return result; }
说明
友元函数是能够访问类中私有成员的非成员函数
友元函数在类内声明,类外定义,定义和使用时不需加作用域和类名,与普通函数无异。
一个函数可以是多个类的友元函数,只需要在各个类中分别声明
友元类
概念
声明一个类是另一个类的友元
格式
在类内声明
friend class 类名;
例
1 2 3 4 5 6 7 8 9 10 11
class Test { friend class X; //X是Test的友元类,X可以访问Test的任意成员 private: int m_num; public: Test(int num = 0) { m_num = num; } };
说明
友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明
友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A 的友元
强制类型转换
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
class Student { public: string name; int age; }; //在成员函数中 operator int() const { return age; } operator string() const { return name; } //在主函数中使用 int age1 = (int)stu1; string name1 = (string)stu2;
重载运算符
概念
在 C++ 中,可以称运算符为运算符函数
即运算符是一种特殊的函数;
它们的名称、定义和调用格式与普通函数有些差别;
运算符函数可以作为某个类的成员函数,也可以作为普通的 C++函数(常作为类的友元函数)
规定
C++ 不允许用户自己定义新的运算符;
不允许改变运算符操作数的个数(自然不允许使用带默认值的参数);
不能改变运算符的运算优先级;
不能改变运算符的运算结合方向。
下列 5 个运算符不允许被重载。
:: 作用域区分符
. 成员访问运算符
.* 成员指针访问运算符
sizeof 数据尺寸运算符
? : 三目条件运算符(唯一的三目运算)
重载运算符时,至少要有一个操作数为用户自定义的类类型(因为对基本数据类型及其指针而言,其定义已经存在,而构成重载必须有不同于基本数据类型的参数)。
重载的运算符函数不能为类的静态成员函数。
须指出的是系统不会将运算符 “+” 与运算符 “=” 自动组合成运算符 “+=”。需要使用运算符 “+=” 时,应该单独重载它
基本原则
- 尽可能地使用引用型形式参数,并尽可能地加以 const 限制。其作用是尽可能地避免拷贝构造形参操作数;尽可能地保护实参操作数;同时使操作符具有与常量运算的能力。
- 尽可能地采用引用返回,其作用是尽可能地避免拷贝构造临时对象。
- 若第一个操作数可能为非本类的对象时,应考虑将运算符重载成类的友元函数;
- 尽可能地保持运算符原有的含义、保持运算符的直观可视性;
- 充分利用类型转换函数、转换构造函数
目的
- 运算符重载的基本指导原则是为了让自定义类的行为和内建类型一样。自定义类的行为越接近内建类型,就越便于这些类的客户使用。例如,如果要编写一个表示分数的类,最好定义+、-、*和/运算符应用于这个类的对象时的意义。
- 重载运算符的第二个原因是为了获得对程序行为更大的控制权。例如,可对自定义类重载内存分配和内存释放例程,来精确控制每个对象的内存分配和内存回收。
- 需要强调的是,运算符重载未必能给类开发者带来方便;主要用途是给类的客户带来方便
分类
普通运算符重载(“
+
”、“-
”、“*
”、“/
”等)前置运算符重载("
++
"、"--
")后置运算符重载("
++
"、"--
")插入运算符重载("
>>
")提取运算符重载("
<<
")总结
* / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , ->* -> () [] new new[] delete delete[]
重载各类运算符
I/O 流操作运算符
系统重载这两个操作符是以系统类成员函数的形式进行的,因此 cout« var 语句可以理解为:cout.operator«( var )
习惯用类外的友元函数重载形式
|
|
双目算术运算符
一般为类内成员函数,友元也可
|
|
下标运算符
必须以类的成员函数的形式进行重载
|
|
如果使用第一种声明方式,操作符重载函数不仅可以访问对象,同时还可以修改对象
如果使用第二种声明方式,则操作符重载函数只能访问而不能修改对象
以 String 类为例
|
|
迭代赋值运算符
|
|
其余类似
关系运算符
|
|
其余类似
前(后)增(减)量运算符(单目运算符)
前置
1 2 3 4 5 6
Student &operator++() { this->age++; this->weight++; return *this; }
后置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Student operator++(int) { Student temp(this->age, this->weight); age++; weight++; return temp; } //或 Student operator++(int) { Student temp(*this); //拷贝构造 ++(*this); //利用前增量运算符函数 return temp; }
其余类似
继承和派生
概念
类的继承就是新类由已经存在的类获得已有特性,类的派生是由已经存在的类产生新类的过程。已有类叫做基类,产生的新类叫做派生类
一个派生类可以有多个基类,叫做多继承;否则为单继承。直接派生出某个类的基类叫做这个类的直接基类,基类的基类或更高层的基类叫做派生类的间接基类
基类的构造函数和析构函数派生类是不能继承的。如果派生类需要对新成员初始化或者进行特定的清理工作,就需要就需要自己定义构造函数和析构函数了。从基类继承的成员的初始化仍可通过基类的构造函数来完成
派生类的数据成员包括从基类继承来的数据成员和派生类新增的数据成员,还可能包括其他类的对象(实际上还间接包括了这些对象的数据成员)作为其数据成员。我们对派生类初始化时需要对基类的数据成员、派生类新增数据成员和内嵌的其他类对象的数据成员进行初始化。
基类的构造函数若有参数,则派生类必须定义构造函数,将传入的参数再传递给基类的构造函数,对基类进行初始化。若基类没有定义构造函数,则派生类也可以不定义构造函数,都使用默认构造函数,对于派生类的新增数据成员可以通过其他的公有函数成员来初始化。而如果基类同时定义了默认构造函数和带参数的构造函数,那么在派生类的构造函数中可以给出基类名及其参数表,也可以不显式给出
格式
class DeriveClass: acess_label BaseClass
例
|
|
继承方式
一般使用公有继承
公有继承
派生类对基类中的公有成员和保护成员的访问属性都不变,而对基类的私有成员则不能访问。(类的对象也属于类外的,不能访问保护成员)
保护继承
基类的公有成员和保护成员被派生类继承后变成派生类的保护成员,而基类的私有成员在派生类中不能访问。
私有继承
基类的公有成员和保护成员被派生类继承后变成派生类的私有成员,而基类的私有成员在派生类中不能访问
派生类的构造函数
说明
派生类的构造函数需要做的工作有,使用传递给派生类的参数,调用基类的构造函数和内嵌对象成员的构造函数来初始化它们的数据成员,再添加新语句初始化派生类新成员。派生类构造函数的语法形式为:
1 2 3 4
派生类名::派生类名(参数表):基类构造函数名1(参数表1),...基类构造函数名m(参数名m),组合成员名(对象参数表1),...,组合成员名n(对象参数表n),其他成员初始化 { 初始化派生类新成员的语句; }
基类的构造函数若有参数,则派生类必须定义构造函数,将传入的参数再传递给基类的构造函数,对基类进行初始化。
若基类没有定义构造函数,则派生类也可以不定义构造函数,都使用默认构造函数,对于派生类的新增数据成员可以通过其他的公有函数成员来初始化。
而如果基类同时定义了默认构造函数和带参数的构造函数,那么在派生类的构造函数中可以给出基类名及其参数表,也可以不显式地给出
执行构造函数的构造顺序
- 首先调用基类的构造函数,若有多个基类,调用顺序按照它们在派生类声明时从左到右出现的顺序
- 如果有内嵌对象成员,则调用内嵌对象成员的构造函数,若为多个内嵌对象,则按照它们在派生类中声明的顺序调用,如果无内嵌对象则跳过这一步;
- 调用派生类构造函数中的语句
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
#include <iostream> using namespace std; class Base1 // 基类Base1,只有默认构造函数 { public: Base1() { cout << "Base1 construct" << endl; } }; class Base2 // 基类Base2,只有带参数的构造函数 { public: Base2(int x) { cout << "Base2 construct " << x << endl; } }; class Base3 // 基类Base3,只有带参数的构造函数 { public: Base3(int y) { cout << "Base3 construct " << y << endl; } }; class Child : public Base2, public Base1, public Base3 // 派生类Child { public: Child(int i, int j, int k, int m) : Base2(i), Base3(j), b2(k), b3(m) {} private: // 派生类的内嵌对象成员 Base1 b1; Base2 b2; Base3 b3; }; int main() { Child child(3, 4, 5, 6); system("pause"); }
基类和内嵌对象成员的构造函数的调用顺序和它们在派生类构造函数中出现的顺序无关
Child(int i, int j, int k, int m) : Base2(i),b3(j),b2(k),Base3(m) {}
结果按照 i,m,k,j 的顺序赋值
派生类的析构函数
说明
派生类的析构函数一般只需要在其函数体中清理新增成员就可以了,对于继承的基类成员和派生类内嵌对象成员的清理,则一般由系统自动调用基类和对象成员的析构函数来完成
执行析构函数的清理顺序(正好和派生类构造函数相反)
- 执行析构函数语句清理派生类的新增成员;
- 调用内嵌对象成员所属类的析构函数清理派生类内嵌对象成员,各个对象成员的清理顺序与其在构造函数中的构造顺序相反;
- 调用基类的析构函数清理继承的基类成员,如果是多继承则各个基类的清理顺序也与其在构造函数中的构造顺序相反。
作用域分辨符
在派生类内部访问基类同名成员的语法形式
基类名::数据成员名; // 数据成员
基类名::函数成员名(参数表); // 函数成员
在派生类外通过派生类对象访问的话,前面还要加上“派生类对象名.”
派生类对象名.基类名::数据成员名; // 数据成员
派生类对象名.基类名::函数成员名(参数表); // 函数成员
类型兼容/复制兼容
说明
赋值兼容规则就是指在基类对象可以使用的地方都可以用公有派生类对象来代替。
可以使用类 Base 对象的地方都可以使用类 Child 的对象来代替。
规则
派生类对象可以赋值给基类对象。也就是将派生类对象从基类继承的成员的值分别赋值给基类对象相应的成员。例如: base = child;
派生类对象的地址可以赋值给基类类型的指针。例如:pBase = &child;
派生类对象可以用来初始化基类的引用。例如:Base &b = child;
公有派生类对象可以代替基类对象使用,但是我们只能使用它从基类继承的成员,而无法使用它的新添成员。
虚函数
说明
- 虚函数就是在类的声明中用关键字 virtual 限定的成员函数
- 虚函数是非静态的成员函数,一定不能是静态(static)的成员函数
- 虚函数的主要作用就是显示的声明基类中的函数可以在派生类中重新定义,也就是说只有成员函数才能被申明为虚函数
- 构造函数不能是虚函数;基类析构函数应当是虚析构函数,即使它不执行任何操作
格式
类内
1 2 3 4
virtual 函数类型 函数名(形参表) { 函数体 }
类外
只能在此成员函数的声明前加
virtual
修饰,而不能在它的定义(实现)前加
虚析构函数
析构函数用于在类的对象消亡时做一些清理工作,我们在基类中将析构函数声明为虚函数后,其所有派生类的析构函数也都是虚函数,使用指针引用时可以动态绑定,实现运行时多态,通过基类类型的指针就可以调用派生类的析构函数对派生类的对象做清理工作。
析构函数没有返回值类型,没有参数表,所以虚析构函数的声明也比较简单,形式如下
1
virtual ~类名();
纯虚函数
即使有的虚函数在基类中不需要做任何工作,我们也要写出一个空的函数体,这时这个函数体没有什么意义,重要的是此虚函数的原型声明。C++为我们提供了纯虚函数,让我们在这种情况下不用写函数实现,只给出函数原型作为整个类族的统一接口就可以了,函数的实现可以在派生类中给出。
抽象类不能实例化
纯虚函数是在基类中声明的,声明形式为
1
virtual 函数类型 函数名(参数表) = 0;
纯虚函数的声明形式与一般虚函数类似,只是最后加了个
=0
。纯虚函数这样声明以后,在基类中就不再给出它的实现了,各个派生类可以根据自己的功能需要定义其实现。
一些要点
类相当于一种新的数据类型,数据类型不占用存储空间,用类型定义一个实体的时候,才会为它分配存储空间
面向对象程序设计过程中一般将数据隐蔽起来,也就是说一般的变量(数据)都声明为 private,而成员函数声明为 public。如果在声明的时候不写访问控制属性,则类会默认它为 private。
在类里面,数据成员不能够进行初始化
对象所占的空间等于基本空间(所有数据成员所占的字节,可以用 sizeof()显示)和资源空间(生成的堆空间)
创建对象数组,先定义的后析构
在类的设计中,若类的对象可能带有资源(数据成员为指针,开辟过堆空间),则应定义深拷贝构造函数,重载赋值运算符以实现深赋值运算,定义析构函数释放资源。
类的三大特性:封装性、继承性、多态性
重载:相同作用域,函数名相同
重写:不同作用域,函数头完全相同
隐藏:不同作用域,函数名相同(返回类型、形参表等可能不同)
基类指针指向派生类对象,再用基类指针调用派生类虚函数,会产生多态(迟后联编)
十一、链表类
|
|