目录
一、类
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事拆分成不同的对象,通过对象之间的交互完成。 对象的共同特征由属性和功能组成,一般使用变量描述共同的属性,使用函数描述共同的功能。
1.类的定义
①使用结构体(struct)描述
C++对结构体的语法进行了扩展,结构体中不但可以有成员变量,也可以有成员函数。因此结构体可以用来描述对象的类型,在C++中,对象的类型叫类。
struct Test
{
//成员变量
int a;
double b;
//成员函数
int Add(int x, int y)
{
return x + y;
}
};
在创建一个变量时可以不需要struct关键字:
Test t; //创建一个对象t
注意:C语言中结构体不可以有函数成员,但可以通过函数指针调用函数。
②使用类(class)描述对象的类型
class也可以用来描述对象的类型,语法和struct几乎一致 。
class className
{
//类体:由成员变量和成员函数组成
}; //注意后面的分号
其中class为定义类的关键字,className为类的名字,{}中为类的主体,注意定义结束时加上后面的分号。类中的元素称为类的成员:类中的数据称为类的属性或者成员变量,类中的函数称为类的方法或者成员函数。
用类类型创建对象的过程,称为类的实例化。,实例化出的对象将占用实际的物理空间来存储类成员变量。
注意:类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外使用成员,需要使用“::”作用域解析符指明成员属于哪个类域。
定义和声明位置:
①声明和定义全部放在类体中。需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
②声明放在头文件(.h)中,定义放在源文件(.cpp)中(推荐)。
类的前置声明:声明一个类名,类中的内容的具体实现实在之后的代码中定义。当类还未定义时需要使用该类类型就可以使用前置声明。
class 类名; //前置声明
2.类的访问限定符
类中的成员具有访问属性,访问属性分为三类:
访问属性 | public --- 公有属性 | protected --- 保护属性 | private --- 私有属性 |
特点 | 可以在类外和类内访问该成员 | 可以在类内部和子类中访问该成员 | 只能在类内部访问 |
struct中的成员的默认访问属性是公开的,class中的成员的默认访问属性是私有的。
C++中的struct和class的区别是什么?
C++需要兼容C语言,所以C++中的struct可以当成结构体去使用。C++中的struct还可以用来定义类,和class定义类是一样的,区别是struct的成员默认访问权限是public,而class的成员默认访问权限是private。
在实际开发中,应该显示指定成员的访问属性:
class 类名{
public:
公有成员;
protected:
保护成员;
private:
私有成员
};
一个类中同个访问属性可以多次出现,通常把公开的成员写在类的最前面,私有成员写在类的最后 。一般来说,成员变量总是设置为私有属性,如果类外需要访问就提供公开的接口成员函数。
3.类的封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
C++实现封装的方式:用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限,选择性的将其接口提供给外部的用户使用。
class Time{
//属性 ----- 成员变量
private://私有成员
int hour;//时
int min;//分
protected://保护成员
int sec;//秒
public://公有成员
//构造函数
Time(int h=23,int m=59,int s=55):hour(h),min(m),sec(s)
{
cout<<"Time()"<<endl;
}
//功能 ----- 成员函数
//如果需要访问私有成员变量,提供对外接口
int get_hour()
{
return hour;
}
void set_hour(int h)
{
hour = h;
}
//设置时间
void set_time(int h=23,int m=59,int s=55)
{
hour = h;
min = m;
sec = s;
}
};
4.this指针
C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问的。只不过所有操作对用户是透明的,即用户不需要来传递,而是编译器自动完成。
#include <iostream>
using namespace std;
class Date
{
public:
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;//实例化两个日期类
d1.SetDate(2021, 5, 25);//设置d1的日期
d2.SetDate(2021, 5, 26);//设置d2的日期
d1.Display();//打印d1的日期
d2.Display();//打印d2的日期
return 0;
}
上述代码调用成员函数传参时,看似只传入了一些基本数据,实际上还传入了指向该对象的指针:
编译器进行编译时,看到的成员函数实际上也和我们所看到的不一样,每个成员函数的第一个形参实际上是一个隐含的this指针,该指针用于接收调用函数的对象的地址,用this指针就可以很好地访问到该对象中的成员变量:
this指针可以在类的成员函数和构造函数中使用,代表的是调用该函数的对象(构造的对象)的地址
在成员函数中,this指针指向调用该函数的对象
在构造函数中,this指针指向正在构造的对象
this指针的作用
①可以在构造函数和成员函数中形参(局部变量)与成员变量重名时使用this指针来进行区分。 ②this可以作为函数的参数和返回值
this指针特点
①this指针的类型:类类型* const。
②this指针只能在“成员函数”的内部使用。
③this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
④this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
注意:类中访问成员变量时,尽量使用this指针,避免发生歧义。
二、类的默认成员函数
如果一个类中什么成员都没有,简称其为空类。但是空类中会自动生成6个默认成员函数。
如果没有重写这些函数,编译器会自动生成。若重写了这些函数,那么编译器不会生成,使用重写后的函数。
1.构造函数
构造函数:名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数参数的传递:如果构造函数有参数,需要在构造对象铜锅小括号传递实参。
Animal an(构造函数的实参);
Animal *pa = new Animal(构造函数的实参);
构造函数的重载和参数默认值:一个类可以有多个构造函数,这些构造函数构成重载关系,在构造对象时选择合适的构造函数去调用。构造函数也可以由参数默认值,但是注意不要和重载冲突。
对象的构造过程 :
1.系统根据对象大小分配内存空间
2.检查成员变量的类型,如果是基本类型就什么都不做,如果是类类型调用该类的构造函数
所以编译器自动生成的构造函数对内置类型不做处理。对于自定义类型,编译器会再去调用它们自己的默认构造函数。
初始化参数列表:类中有引用成员、const成员必须在调用构造函数前初始化,这时需要使用初始化参数列表。
初始化参数列表在构造函数的形参列表之后,函数语句体之前,使用初始化参数列表可以在调用构造函数之前对成员进行初始化,语法如下:
class A{
public:
A(...):/*初始化参数列表*/{
//...
}
};
//初始化参数列表
构造函数(形参列表):初始成员1(值1),初始化成员2(值2),...{
函数语句体;
}
构造函数特点
①构造函数的函数名与类名相同
②构造函数无返回值
③对象实例化时编译器自动调用对应的构造函数
④构造函数支持重载
⑤无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无需传参就可以调用的构造函数就是默认构造函数。
⑥如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成
注意:这里所说的构造函数无返回值是真的无返回值,而不是说返回值为void。
2.析构函数
析构函数:析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数特点:
①析构函数是一个特殊的函数,函数名和类名相同,但是要在前面加~,既没有参数,也没有返回值。
class Date
{
public:
Date()// 构造函数
{}
~Date()// 析构函数
{}
private:
int _year;
int _month;
int _day;
};
②对象生命周期结束时,C++编译器会自动调用析构函数
③如果类中没有析构函数,编译器会生成一个什么也不做的析构函数
④一个类有且只有一个析构函数。若未显示定义系统会自动生成默认的析构函数。如果类中有析构函数,编译器不再做该动作。
编译器自动生成的析构函数机制:
①编译器自动生成的析构函数对内置类型不做处理。
②对于自定义类型,编译器会再去调用它们自己的默认析构函数。
⑤先构造的后析构,后构造的先析构
对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则。
需要实现析构函数的情形
①如果在销毁对象的时候需要释放资源就要重写析构,比如堆内存,硬件设备,文件....
②在构造函数中申请了内存(new),在析构函数中就要delete
3.拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用从const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数是一个特殊的构造函数,使用一个已有对象去初始化一个新建对象时,调用拷贝构造函数。
//A是类类型
A a;//构造函数
A b = a; //拷贝构造函数
A c;
c = a;//不是拷贝构造
实现语法
class A{
public:
A(...){...}//构造函数
/*拷贝构造*/
A(const A &a){...}
};
拷贝构造函数调用的时机
1.使用已有的对象去初始化新对象 A a; A b = a;//拷贝构造
2.把一个对象传递给本类型的形参
3.把一个对象作为函数的返回值(编译器会优化成传递构造)
什么情况需要重写拷贝构造函数?
若果一个类没有拷贝构造函数,编译器会自动生成一个按逐字节拷贝的拷贝构造函数。
如果希望自定义拷贝构造的过程可以重写拷贝构造函数,在对象中存在独立的内存(资源),需要重写拷贝构造,如指针成员。
默认的拷贝构造函数属于浅拷贝。为独立内存重新分配空间,并拷贝空间中的数据就叫深拷贝。
浅拷贝只会将内存地址发展一份,两个对象的指针成员共用一个地址,这明显不符合程序的要求。因为我们希望两个对象之间的任何操作都不能够互相影响。所以在对象中存在独立的内存资源,需要重写拷贝构造。
class Date{
public:
Date(int year=2022,int month=3,int day=14,const char *s="hello"):year(year),month(month),day(day)
{
this->str = new char[strlen(s)+1];
strcpy(this->str,s);
cout<<"Date()"<<endl;
}
//拷贝构造
Date(const Date &dt)
{
cout<<"Date(const Date &dt)"<<endl;
//使用参数对象的数据去初始化新建对象的数据
//非指针类型的成员直接拷贝赋值
this->year = dt.year;
this->month = dt.month;
this->day = dt.day;
//指针类型的成员要重新分配空间,拷贝指向的数据
this->str = new char[strlen(dt.str)+1];
strcpy(this->str,dt.str);
}
~Date()
{
cout<<"~Date()"<<endl;
delete[] this->str;
}
private:
int year;
int month;
int day;
char *str;
};
拷贝构造函数特点
①拷贝构造函数是构造函数的一个重载形式。
②拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
③编译器自动生成的拷贝构造函数不能实现深拷贝
编译器自动生成的拷贝构造函数会对内置类型完成浅拷贝。浅拷贝实际上就是将一个对象的内容完完全全的复制了一份拷贝给另外一个对象,所以浅拷贝也叫做值拷贝。 若存在独立的内存资源,一般希望深拷贝,为另外一个对象开辟一个新的空间,并且浅拷贝会导致析构两次、程序崩溃等问题,需要我们自己写对应的拷贝构造函数。
三、类中的特殊成员
1.const修饰成员和对象
const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。const修饰类成员变量也不可以被修改。
class A{
public:
A(int n=0,int m=0):num(n),num_1(m){}
void show(){....}
void show()const{....}//const成员函数
private:
const int num;//const成员变量
int num_1;
};
A a;
const A b;//const对象
const成员函数和同名的非const成员函数构成重载关系,非const对象优先调用非const函数,const对象只能调用const函数。
const成员函数只能读取成员变量,不能修改成员变量。如果一定要修改,必须在声明该成员变量前加mutable修饰符。
mutable int x; //允许在const成员函数中修改
所以如果一个成员函数不会修改成员变量,就应该将其设计为const成员函数。
注意:const成员变量和const对象都不能修改。
1.const对象可以调用非const成员函数吗? 不可以
非const成员函数,即成员函数的this指针没有被const所修饰,传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败
2.非const对象可以调用const成员函数吗? 可以const成员函数,即成员函数的this指针被const所修饰,传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功
3.const成员函数内可以调用其他的非const成员函数吗? 不可以在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗? 可以在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。
2.static静态成员
静态成员分为静态成员变量和静态成员函数,静态成员属于类,不属于某个对象。
静态成员变量在成员变量声明前加static,必须初始化,而且应该在类外初始化,默认初始化为0,类类型的静态成员变量调用默认构造函数。
静态成员变量在成员函数声明前加static,静态成员函数只能访问静态成员变量,不能访问普通成员变量。
class A{
public:
static int num;//静态成员变量
static void show(){......}//静态成员函数
};
//类外初始化
int A::num = xxx;
//访问静态成员
类名::静态成员;
静态成员无需通过对象来访问(也可以通过对象来访问),可以直接通过类名访问。实现的机制是静态成员存储在独立的内存中,不能在在静态成员函数中使用this指针。所有同类型的对象访问的是同一个静态成员。
static静态成员特点
①静态成员为所以类对象所共享,属于整个类,不属于某个具体的对象
②静态成员变量必须在类外定义,定义时不添加static关键字
③静态成员函数没有隐藏的this指针,不能访问任何非静态成员
注意:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
①静态成员函数可以调用非静态成员函数吗?不可以
因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
②非静态成员函数可以调用静态成员函数吗? 可以
因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制
3.友元
友元分为友元函数和友元类。友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元的作用就是让类外数据突破访问权限的限制,可以将 类/函数 声明为某个类的友元,友元类和友元函数可以访问类中的所有数据,不受访问权限的限制。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,属于全局函数,但需要在类的内部声明该函数为友元函数,声明时需要加friend关键字。
在类内部声明: friend 函数声明;
友元函数特点
①友元函数可以访问类是私有和保护成员,但不是类的成员函数。
②友元函数不能用const修饰。
③友元函数可以在类定义的任何地方声明,不受访问限定符的限制。④一个函数可以是多个类的友元函数。
⑤友元函数的调用与普通函数的调用原理相同。
友元类:友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。有一个类A,在类A中将类B声明为友元,类B就可以访问类A中的所有数据,语法如下:
在类的内部声明: friend class 友元类名;
class A
{
// 声明B是A的友元类
friend class B;
public:
A(int n = 0):_n(n)
{}
private:
int _n;
};
class B
{
public:
void Test(A& a)
{
// B类可以直接访问A类中的私有成员变量
cout << a._n << endl;
}
};
友元类特点
①友元关系是单向的,不具有交换性。
若B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。②友元关系不能传递。
如果A是B的友元,B是C的友元,不能推出A是C的友元。
注意:友元不受访问权限的限制,可以访问类中的所有数据,破坏了类的封装属性,如非必要,不要使用。
4.内部类
如果一个类定义在另一个类的内部,则这个类被称为内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类。外部类对内部类没有任何优越的访问权限。内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
#include <iostream>
using namespace std;
class A //外部类
{
public:
class B //内部类
{
private:
int _b;
};
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl; //外部类的大小
return 0;
}
内部类特点
①内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。
②内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
③外部类的大小与内部类的大小无关。