大家都知道C语言是面向过程的,那如果想要更加安全地封装我们的代码只对外暴露一个接口,这时就需要通过C++面向对象来实现。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
C++中类有两种:class(私有类)和struct(公有类)。
class的默认访问权限为private,struct为public(因为struct要兼容C)。
比如C语言中的结构体定义:
//struct ListNode是类型
typedef struct ListNode {
int val;
struct ListNode* next;
}LTN;
而C++把struct升级为类:
//ListNode是类名
struct ListNode {
int val;
ListNode* next;
};
或者
typedef struct Node {
int data;
struct Node* next;
}Node,*PNode;//PNode就是Node*
类中既有成员变量,又有成员函数。比如:
class Date {
public:
//成员函数
void Init(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
void func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
private:
//成员变量
int _year;//声明
int _month;
int _day;
};
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
注意:
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
语法:类型& 引用变量名(对象名) = 引用实体;
int i = 0;
int& k = i;//引用
引用的作用:
1.输出型参数
我们在C语言中学过想要交换两个数的大小不能用值传递,而要用地址传递,而每次调用函数我们必须要加&取地址符显得很麻烦,所以就可以用引用作输出型参数--形参的改变影响实参
void Swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
2.做返回值
//传值返回
int Count() {
//static int n=0;
int n = 0;
n++;
return n;
}
变量n在静态区,栈帧销毁不会影响,也是把n拷贝给临时变量再拷贝给主函数。
变量n在栈帧销毁前拷贝给主函数里的临时变量(放在寄存器里,临时变量的类型就是函数返回值的类型),临时变量再拷贝给主函数中变量,这样会增加拷贝,所以我们可以用引用做返回值:
//传引用返回
int& Count1() {
static int n = 0;
n++;
return n;
}
引用返回就是给n起了个别名(相当于n)然后传给主函数,减少了拷贝。
传引用返回:1.减少了拷贝 2.调用者可以修改返回对象
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参。
缺省参数分为全缺省和半缺省:
//全缺省参数
void func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
//半缺省参数
//必须从右往左连续缺省
void func2(int a, int b = 10, int c = 20)
//void func2(int a = 10, int b, int c = 20)//错误的
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
调用函数的时候可以修改赋的值:
func(1, 2, 3);// a=1 b=2 c=3
func(1, 2);// a=1 b=2 c=30
func(1);//a=1 b=20 c=30
func();//a=10 b=20 c=30
用类类型创建对象的过程,称为类的实例化。比如:
Date d1;
Date d2;
如果是静态成员函数,需要通过类名来调用;如果是非静态成员函数,则需要通过对象实例化来调用。比如定义一个MyClass类:
class MyClass {
public:
static void staticFunction() {
//...
}
void nonStaticFunction() {
//...
}
};
MyClass::staticFunction(); // 调用静态成员函数,MyClass::是MyClass类的作用域
MyClass obj;
obj.nonStaticFunction(); // 调用非静态成员函数
类对象的大小也可以通过结构体内存对齐规则来计算出来,不知道的朋友可以看看我之前的结构体那一篇博客。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数(this),让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
那当d1调用 Init 函数时,编译器就会给d1的地址传给Init 函数中隐含的形参this指针来接收,不需要用户传递。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数分为无参构造和有参构造:
class Date {
public:
Date() {//无参构造
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month, int day) {//有参构造
_year = year;
_month = month;
_day = day;
}
private:
//声明位置给的缺省值
int _year=1;//声明,不是定义
int _month=1;
int _day=1;
};
Stack st;//无参构造
Stack st(4);//有参构造 相当于Stack st; st.Stack(4);
我们也可以利用前面学过的缺省参数写出缺省参数构造既实现初始化又实现值传递:
Date(int year=1, int month=1, int day=1) {
_year = year;
_month = month;
_day = day;
}
初始化列表:
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始 化一次,而构造函数体内可以多次赋值。所以为了实现对象整体的初始化,我们需要用初始化列表。
语法:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
比如:
class A {
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B {
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
注意:三类成员变量(const/引用/没有默认构造的自定义类型成员)必须初始化。
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
~Stack() {
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
拷贝分为浅拷贝和深拷贝:
内置类型:编译器可以直接拷贝(浅拷贝),自定义类型:需要调用拷贝构造(深拷贝)。
拷贝构造:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
//Date(const Date d) // 错误
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
Date d1(2024, 1, 29);
Date d2(d1);//把d1拷贝给d2
Date d3=d1;//把d1拷贝给d3
运算符重载--内置类型赋值(=)和自定义类型取地址(&)不需要重载,编译器会默认生成
语法:返回值类型 operator操作符(参数列表)
比如==运算符重载:
bool operator==(const Date& d1, const Date& d2) { // d1 == d2 -> d1.operator==(d2)
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
<运算符重载:
bool operator<(const Date & d) { // d1 < d2 -> d1.operator<(d2)
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
>=运算符重载:
bool operator>=(const Date& d) {
return !(*this < d);
}
注意: .* :: sizeof ?: . 这5个运算符不能重载。
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
void Print() const {
cout << _a << endl;//权限平移
}
class A {
public:
void Print() const {
cout << _a << endl;//权限平移
}
A* operator&() {
return (A*)0x01;//整型强转成指针
}
const A* operator&() const{
return this;//返回值只读 权限缩小
}
private:
int _a = 10;
};
通过取地址操作符重载可以随意修改内容,比如防止别人访问。
友元分为:友元函数和友元类
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
class Date
{
//友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const;
private:
int _year;
int _month;
int _day;
};
友元函数可访问类的私有和保护成员,但不是类的成员函数友元函数不能用const修饰。 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量,而Time类不能访问日期类的私有成员变量。
友元关系是单向的,不具有交换性,不能传递和继承。
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类就是外部类的友元类。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元,可以访问A类的私有
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
private:
int b = 2;
};
};
int A::k = 1;
内部类--跟A类空间是独立的,只是受A的类域和访问限定符(私有类不能访问)的限制。
编译器优化
优化不产生临时对象,在同一个表达式里拷贝和拷贝构造才会优化。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A aa) {
}
void func2(const A& aa) {
}
A func3() {
A aa;
return aa;
}
A func4() {
return A();
}
A aa1 = 1;//构造+拷贝构造->优化为直接构造
func1(aa1);//无优化,只是拷贝构造
func1(2);//构造+拷贝构造->优化为直接构造
func1(A(3));//构造+拷贝构造->优化为直接构造
func2(aa1);//无优化
func2(2);//无优化
func2(A(3));//无优化
func3();//构造+拷贝构造
A aa1 = func3();//构造+拷贝构造+拷贝构造->优化为构造+拷贝构造
A aa2;
aa2 = func3();//aa2和aa1是一样的,但是不能优化
func4();//构造+拷贝构造->优化为直接构造
A aa3 = func4();//构造+拷贝构造+拷贝构造->优化为直接构造
如果觉得这篇文章对你有帮助可以收藏下来,也欢迎大家进行批评指正,理解类和对象可以帮助我们更好的编写程序,一起加油