导图社区 CPP《类的知识点》
c 专题知识点框架,内容有类的构成、类的声明、成员函数的声明、对象、构造函数、析构函数、嵌套类、类前引用... ...
编辑于2023-05-30 21:16:56 江苏省类
结构体与类的区别
结构体只能放数据成员 类既可以放数据成员也可以放成员函数
结构体默认权限是公有的,无法设定权限 类默认数据和函数是私有的,可以设置权限
类的构成
数据成员
成员函数
类的声明
格式
class 类名 { public: 公有数据成员; 公有成员函数; protected: 保护数据成员; 保护成员函数; private: 私有数据成员; 私有成员函数; };
权限
public
公有权限,类内类外都可以访问
protected
保护权限,类外不可访问,派生类继承后可访问
private
私有权限,类外不可访问,派生类继承不可访问,(友元可以访问)
注意事项
缺省状态默认为private
数据成员可以是任何数据类型,但不能用自动(auto)、寄存器(register)或外部(extern)进行声明。
声明里面,不能给类成员数据赋值
成员函数的声明
形式
普通函数形式
类内声明
class Coord { public: void setCoord (int,int); // 设置坐标点 int getx(); // 取x坐标点 int gety(); // 取y坐标点 private: int x,y; };
类外实现
void Coord∷setCoord(int a,int b){ x=a; y=b;} int Coord::getx(){ return x;} int Coord::gety(){ return y;}
内联函数形式
隐式声明
直接将函数声明在类内部。
class Coord{ public: void setCoord(int a,int b) { x=a; y=b;} int getx() { return x;} int gety() { retrun y;} private: int x,y; };
显式声明
类内声明
class Coord{ public: void setCoord(int,int); int getx(); int gety(); private: int x,y; };
类外实现
inline void Coord::setCoord(int a,int b) { x=a; y=b;} inline int Coord::getx(){ return x;} inline int Coord::gety(){ return y; }
二者区别
1. 内联函数和普通函数的参数传递机制相同,但是编译器会在每处调用内联函数的地方将内联函数内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷。
2. 普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有N次调用了内联函数则会有N次展开函数代码。
3. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句。如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行
成员函数可以访问类中全部成员数据和其他函数
对象
定义
类的变量叫做类的对象,对象也称为类的实例
创建方法
声明的同时,也创建
class Coord { public: void setCoord(int,int); int getx(); int gety(); private: int x,y; } op1,op2;
全局对象
先声明,再创建
class Coord { //… }; // … main() { Coord op1,op2; // … }
局部对象
类的声明只作为一个模板,只有定义了对象后,才会分配存储空间。
成员访问
特点
在类的外部可以通过类的对象对公有成员进行访问,但无法访问私有成员
在类的内部所有成员之间都可以通过成员函数直接访问
一般形式 (缩写形式)
对象名.数据成员名
对象名.成员函数名(参数表)
其中“.”叫做对象选择符,简称点运算符。
完整形式
对象名.类名::成员名
对象名.类名::成员函数名(参数表)
对象为指针 成员访问
一般形式
date3->setDate(2001,8,15);
Date *date1=new Date(1998,4,28);
对象的赋值
同类型的对象间也可以进行赋值,当一个对象赋值给另一个对象时,所有的数据成员都会逐位拷贝。
例子
#include<iostream.h> class abc{ public: void init(int i,int j) { a=i; b=j; } void show(){ cout<<a<<" "<<b<<endl; } private: int a,b; }; main() { abc o1,o2; o1.init(12,34); o2=o1; // 将对象o1数据成员的值赋给对象o2 o1.show(); o2.show(); return 0; }
说明
必须同类型
仅仅使对象中的数据成员发生拷贝,但仍然是两个不同的对象实体
注意,赋值运算符,即等于号,在定义类时,如不对赋值运算符进行重载,则会按默认的缺省方式定义赋值运算符,即上述的,逐位拷贝
逐位拷贝在拷贝指针型的成员数据时,可能会带来逻辑上的问题,问题是析构带来的,两个指针被指向了同一片内存区域,本质上是一种浅拷贝,因此析构时会有重复析构产生报错
通过对象访问类中数据或函数只能访问public权限的
构造函数
目的
为了实现类中数据成员的初始化
两种初始化思想
传统初始化思路
在类中写一个public权限的成员函数,因为成员函数可以访问全部的成员数据,也可以与外界通过参数进行传递,因此可以实现成员数据的初始化赋值。
声明类→定义对象→调用接口给成员赋值
构造函数的初始化思路
由于构造函数与类同名,因此在创建类对象的同时,会自动执行构造函数,同时也就实现了初始化
声明类→定义对象→同时给成员赋值
显然,构造函数的初始化思路要更简洁 且更贴合代码习惯,创建对象后立刻初始化。
特点
没有返回类型,void也不行
可以有参数,也可以没有
函数名必须和类名完全一样
构造函数不可被继承
分类
无参构造
定义
参数列表为空的构造函数
特点
有且只能有一个无参构造,若不自定义,则系统会标配一个默认的
有参构造
定义
参数列表不为空的构造函数
特点
与普通函数一样,可以根据重载机制,发生各种版本的重载
拷贝构造
定义
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。 其作用是使用一个已经存在的对象去初始化另一个同类的对象。
特点
该函数只有一个参数,并且是同类对象的引用。
每个类都必须有一个拷贝构造函数。程序员可以根据需要定义特定的拷贝构造函数,以实现同类对象之间数据成员的传递。如果程序员没有定义类的拷贝构造函数,系统就会自动生成产生一个缺省的(默认的)拷贝构造函数。
默认拷贝构造函数为浅拷贝,按位复制
例子
class Coord{ int x,y; public: Coord(const Coord& p) // 拷贝构造函数 { x=2*p.x; y=2*p.y; cout<<"Using copy constructor\n"; } };
调用方式(2种)
创建对象时
class A{}; A a1; A a2(a1);
赋值运算符
class A{}; A a1; A a2; a2 = a1;
意想不到的情况
函数的传参
形参为类对象,进入函数体之后,会备份一份,这时候执行的是拷贝构造。
函数的返回值
返回值为类对象,函数体返回结果时,会备份一份,将备份的结果返回(原有结果在函数运行结束后销毁),此时也是用的拷贝构造复制。
浅拷贝与深拷贝
浅拷贝
按位复制,在复制指针时会产生错误,指针会被原封不动的复制一份,即拷贝之后,复制体与被复制体会指向同一块内存,而不是内存本身发生了复制。
潜在的问题是,在析构的时候,复制体与被复制体的某一方先执行析构后,内存区域被销毁了,那另一方后执行的,就会出现野指针(所指向的内存区域不存在了),再调用析构时,会因为无法找到内存区域,而报错。
深拷贝
通过自定义拷贝构造函数,优化浅拷贝存在的问题。对任何指针类型,数组类型(开辟在堆区的变量)进行赋值操作时,先new一个堆区内存并让指针指向它,再将待复制的内存的值按位复制过来。本质上是两个具有相同值的不同内存区域。这样再各自析构时,是相互独立的,不会报错。
默认构造函数
分类
默认无参构造
会根据成员数据的属性赋值,有可能是无意义的随机值
默认拷贝构造
参数为类的引用类型成员,拷贝方式为浅拷贝(按位复制)
不产生默认构造的情况
三种情况
用户未定义任何构造,两种默认构造都会自动生成
用户定义了拷贝构造,则两种默认构造不再自动生成
用户未定义拷贝构造,但定义了有参或无参构造,则默认无参构造不再自动生成
一个特殊情况
用户用具有全参缺省默认值的方式定义了有参构造,则系统会将此构造方式作为默认的无参构造,不再自动生成默认的无参构造。
test(int i = 1, int j = 1, int k = 1) { first = i; second = j; third = k; }
构造函数的重载
一个类中可以定义多个参数个数或参数类型不同的构造函数
无参构造函数是默认构造函数,一个类只能有一个默认构造函数,因此若要自定义,也只能自定义一个。
尽管一个类可以由很多个重载的构造函数版本,但对象创建时,只会执行其中的某一个,所以发生二义性时,仍然会报错。
构造函数的初始化实现(2种)
传统写法
Date::Date(int y, int m, int d) { year = y; month = m; day = d; }
初始化列表写法
Date::Date(int y,int m,int d):year(y), month(m),day(d) {}
类成员是按照它们在类里被声明的顺序初始化的 与它们在初始化表中列出的顺序无关。
#include<iostream.h> class D { public: D(int i):mem2(i),mem1(mem2+1) { cout<<"mem1: "<<mem1<<endl; cout<<"mem2: "<<mem2<<endl; } private: int mem1; int mem2; }; void main() { D d(15); }
mem1: -858993459 mem2: 15
析构函数
目的
为了实现类中数据成员的销毁(主要是针对堆区内存)
特点
析构函数与构造函数名字相同,但它前面必须加一个波浪号(~);
析构函数没有参数,也没有返回值,而且不能重载。因此在一个类中只能有一个析构函数;
未自定义时,系统会标配一个默认析构函数,但默认析构函数,不会清理堆区的内存。
调用规则
若一个对象被定义在一个函数体内(相当于临时变量),当这个函数结束时,析构函数被自动调用。
若一个对象(指针)是使用new运算符动态创建,再使用delete释放时,自动调用析构函数。
执行顺序
一般顺序
析构函数的执行顺序与构造函数相反,先构造的对象,最后析构
全局对象
只在main函数结束,或者exit函数执行时,调用析构
局部对象
函数结束时,自动调用析构
static局部对象
只会执行一次构造函数,在函数运行结束后,不会析构,只会在main函数结束或exit时,才析构
嵌套类
类中某个数据成员是另一个类的对象
例子
class Point { private: float x,y; //点的坐标 public: Point(float h,float v); //构造函数 float GetX(void); //取X坐标 float GetY(void); //取Y坐标 void Draw(void); //在(x,y)处画点 }; //...函数的实现略 class Line { private: Point p1,p2; //线段的两个端点 public: Line(Point a,Point b); //构造函数 Void Draw(void); //画出线段 }; //...函数的实现略
嵌套类构造函数
原则
不仅要负责对本类中的基本类型成员数据赋初值,也要对对象成员初始化。
例子
class Son { public: int s1; int s2; public: Son(int s1, int s2) { cout << " son 的调用" << endl; this->s1 = s1; this->s2 = s2; } }; class Father { public: int a; int b; Son son; public: Father(int s1,int s2, int a, int b) :son(s1, s2),a(a),b(b) { cout << "father 的调用" << endl; } };
类前引用
定义
类应该先声明,后使用
如果需要在某个类的声明之前,引用该类,则应进行前向引用声明。
前向引用声明只为程序引入一个标识符,但具体声明在其它地方。
示例
class B; //前向引用声明 class A { public: void f(B b); }; class B { public: void g(A a); };
局限性
使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,但是在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。但可以创建该类的指针。
示例
class Fred; //前向引用声明 class Barney { Fred x; //错误:类Fred的声明尚不完善 }; class Fred { Barney y; };
class Fred; //前向引用声明 class Barney { public: void method() { x->yabbaDabbaDo(); //错误:Fred类的对象在定义之前被使用 } private: Fred* x; //正确,经过前向引用声明,可以声明Fred类的对象指针 }; class Fred { public: void yabbaDabbaDo(); private: Barney* y; };