燕子去了,有再来的时候;杨柳枯了,有再青的时候;桃花谢了,有再开的时候。但是,聪明的,你告诉我,我们的假期为什么一去不复返呢?
目录
一、初识类
1.1 类的定义
1.2 C++中的struct
1.3 类的访问限定符
1.4 类域
1.5 对类的对象的成员的访问方式
1.6 this指针
1.7 实例化与对象大小
二、再探类
2.1 类的默认成员函数
2.2 构造函数
2.3 析构函数
2.4 拷贝构造函数
2.5 运算符重载
2.6 赋值运算符重载
2.7 const成员函数
一、初识类
1.1 类的定义
C++中,有一个特殊的关键字,叫作"类",它源自于C语言的结构体,但比结构体要更加高级,比如,在C语言中的结构体内不能定义函数,而C++中的类却可以。
类的定义的关键字为“class”,类的内部包括成员函数与成员变量,它的基本结构如下:
class class_name
{
public:
void Fun1()
{
//...
}
//...
private:
int _a;
int _b;
//...
};
与C语言的结构体类似,类的“}”后也需要加“ ;”,否则编译器会报错。
PS:在上面的举例中,你一定会对“public”与“private”产生好奇,它们是什么含义我们稍后再说,但现在请你记住一点:成员函数一般放在public内,而成员变量一般放在private中。
1.2 C++中的struct
在C++中,结构体struct升级为“类”,也就是说,C++中的struct内可以定义函数,可以进行一系列在class中进行的操作。
1.3 类的访问限定符
在类中储存的内容多种多样,既有公共安全的,也有私密不想被外界访问的,如何将在同一个类的它们区分开,使外部对它们有不同的访问权限呢?访问限定符可以很好的解决这个问题。
访问限定符共有三种:private、protected、public。使用方式是:访问限定符 + ' : '
访问限定符的作用域是从自己起到下一访问限定符的出现,或者是遇到class的' } '结束。
一般来说,在类中,我们会将成员函数放在public(公共的)中,将成员变量放在private/protected(私有的/受保护的)中。
例1:
class Example
{
public:
void fun1()
{
//...
}
int fun2(int a,int b)
{
//...
}
private:
int _day;
int _month;
int _year;
};
这样做的结果就是,外界可以直接调用类里的成员函数,但无法直接访问类的成员变量,并对它进行修改。
1.4 类域
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义类的成员时,需要使用类名+ :: (作用域操作符)以指明所定义的成员属于哪个类域。
例2:
#include <iostream>
using namespace std;
class Example
{
public:
void fun1();
void Print()
{
cout << _a <<" " << _b << endl;
}
private:
int _a;
int _b;
};
void Example::fun1()
{
_a = 1;
_b = 1;
}
int main()
{
Example a;
a.fun1();
a.Print();
return 0;
}
我们在类内对成员函数fun1进行了声明,但是在类外对成员函数fun1进行了定义,因而使用Example::对其加以限定。
1.5 对类的对象的成员的访问方式
与C语言中的结构体相同,有两种,一种是对象名 + ' . ' + 成员,一种是对象的地址 + ' -> ' + 成员
以例2中的对象a为例,想访问a中的fun1函数有两种形式:
a.fun1();//1
Example* pa = &a;
pa->fun1();//2
1.6 this指针
我们运行例2中的代码,运行结果为:
这对于初学C++的人来说还是挺不可思议的,按照我们所学的C语言知识,fun1中的_a与_b应该与a中的_a与_b不是同一个变量,那么fun1对_a与_b修改就不会影响a中的成员变量_a与_b,但结果却是影响了,难道C++独树一帜,对C语言的语法进行大肆修改了?其实不然。
有上述疑问的人很正常,而有这样疑问的人恰恰证明你的C语言学的很扎实。
例2中的fun1与Print函数在编译器编译后的真正内容为:
void Example::fun1(Example* const this)
{
this->_a = 1;
this->_b = 1;
}
void Print(Example* const this)
{
cout << this->_a <<" " << this->_b << endl;
}
也就是编译器之后给我们加上的内容。
这样是不是就很熟悉了?这里的this指向的就是对象a。但是值得注意的是,在编译器中,不能在实参和形参的位置显示的写this指针,不要问为什么,这是规定。但是可以在函数体内显示使用this指针,所以上述内容应修改为:
void Example::fun1()
{
this->_a = 1;
this->_b = 1;
}
void Print()
{
cout << this->_a <<" " << this->_b << endl;
}
这样编译就不会报错,且实现效果与例2一模一样。
我们从中也可以看出C++的优势,相对于C语言,C++省略了一些繁琐的地址传参,更加简洁。
如果你充分了解了内部的机制,就不需要再在函数体内显示使用this指针,心里明白是为什么就可以了,该省劲的地方咱就省劲。
1.7 实例化与对象大小
用类类型在物理内存中创建对象的过程,称为 类实例化出对象。例如例2中类Example实例化出的对象a。类相当于工程图纸,而对象则是按照工程图纸建造出的建筑。
接着介绍如何对对象大小进行计算,首先,对象大小的计算仅包含其成员变量,因为成员函数在对象上的调用本质上是对该函数地址的调用,不需要每一个该类的对象都储存一个相同的函数,所以成员函数不占用对象所属的空间,而是存放在它处。至于成员变量,则是按照内存对齐规则进行计算,这里不再花费篇幅对内存对齐规则进行介绍,不懂的可以自行百度。
那么我们就可以计算得到例2中对象a的大小,为8字节。
到这里类的最基本的知识就介绍完毕了。
二、再探类
2.1 类的默认成员函数
什么是默认成员构造函数?默认成员构造函数就是用户没有显式实现,编译器自动生成的成员函数。一个类在未写成员函数的情况下编译器会自动生成六个默认成员函数,分别是构造函数,析构函数,拷贝构造函数,赋值重载函数,对普通对象取地址重载函数,对const对象取地址重载函数。最后两个函数很少会自己实现,所以我们先注重前四个默认成员函数,即构造函数、析构函数、拷贝构造函数、赋值重载函数。
接下来让我们分别对其进行介绍。
2.2 构造函数
构造函数的作用是对象实例化时对对象进行初始化,它的出现是为了代替C语言中模拟实现像如Stack、Queue中写的Init函数的功能。构造函数自动调用的特点就可以作为Init函数的上位替代了。
接下来简单介绍构造函数的特点:
1. 函数名与类名相同
2. 无返回值,不写函数的返回值类型(void也不要写)
3. 对象实例化时系统会自动调用其对应的类中的构造函数。
4. 构造函数可以重载。
5. 若用户不显式定义构造函数,编译器会自动生成一个无参的默认构造函数;若用户显式定义构造函数,则编译器不会再生成默认的构造函数。
6. 编译器自动生成的构造函数对内置类型成员不做处理,对自定义类型成员会调用它所在类的构造函数
无参构造函数,全缺省构造函数,编译器默认生成的构造函数,均叫作默认构造函数(即不传实参就可调用的构造函数)
构造函数的使用举例:
#include <iostream>
using namespace std;
class Date
{
public:
Date() //无参构造函数,与全缺省构造函数只能存在一个
{
_day = 0;
_month = 0;
_year = 0;
}
Date(int year, int month, int day) //带参构造函数
{
_day = day;
_month = month;
_year = year;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Date a;
a.Print();
Date b(2024, 9, 16);
b.Print();
return 0;
}
代码运行结果:
注意:如果通过无参构造函数创建对象时,对象后不能跟括号,否则编译器无法区分是函数声明还是对象的实例化。
2.3 析构函数
析构函数的功能与构造函数相反,C++规定,对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能相当于C语言中Stack实现的Destroy功能,而部分对象的销毁没有资源需要释放,那么析构函数对于它们来说就是可有可无的。
析构函数的特点:
1. 函数名是在类名前加字符~
2. 无函数参数,无返回值,不需要写返回类型(void也不用)
3. 一个类仅能有一个析构函数。如果析构函数未被显式定义,编译器会自动生成默认的析构函数。
4. 对象的生命周期结束时,编译器会自动调用析构函数。
5. 编译器自动生成的析构函数对内置类型成员不做处理,对自定义类型成员会调用它所在类的析构函数。
6. 显式写析构函数,对于自定义类型成员也会调用它所在类的析构函数,即对于自定义类型成员,显式定义与编译器自动生成都会做出相同的处理:调用它所在类的析构函数。
7. 如果类中没有资源申请,析构函数可以不写,直接使用编译器生成的默认析构函数即可;如果有资源申请,一定要显式写析构函数(自己写),否则会造成资源泄露。
8. 一个局部域的多个对象,C++规定后定义的先析构。全局域的析构晚于局部域。
对于第八点的举例:
#include <iostream>
using namespace std;
class Exa
{
public:
Exa(char x='a')
{
_a = x;
}
~Exa()
{
cout <<"~Exa"<<_a<< endl;
}
private:
char _a;
};
Exa a('A');
int main()
{
Exa b('B');
Exa c('C');
return 0;
}
Exa d('D');
代码运行结果:
2.4 拷贝构造函数
如果一个构造函数的第一个参数是对自身类的类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数。拷贝构造是一个特殊的构造函数。
拷贝构造函数的特点:
1. 拷贝构造函数是构造函数的一个重载。
2. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型的传值传参和传值返回都会调用拷贝构造完成。
3. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会报错,因为在语法逻辑上会引发无穷递归调用。(参照第二点)拷贝构造函数可以有多个参数,除第一个参数,后面的参数必须要有缺省值。
4. 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造函数对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它所在类的拷贝构造函数。
5. 浅拷贝即可完成需求的类不需要写拷贝构造函数,而举例像如Stack(模拟实现栈的类)需要深拷贝,因为一个栈拷贝另一个栈不只有值拷贝,它的地址应不同于拷贝的栈,这时就需要深拷贝,即我们显式写类的拷贝构造函数,以完成深拷贝的需求。
6. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝,但是如果返回对象是一个当前函数局部域的局部对象,函数结束时该对象就被销毁了,那么此时使用引用返回是有问题的,这时的引用相当于野引用。因此,传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后未被销毁,才能返回引用,否则就返回值。
Ps 关于第三点中所提到的拷贝构造函数传值方式传参会引发无穷递归调用:
拷贝构造函数简单使用场景举例:
#include <iostream>
using namespace std;
class Stack
{
public:
Stack(int x = 0,int y = 0,int z = 0) //默认构造函数
{
_capacity = 3;
_top = 3;
a = (int*)malloc(sizeof(int) * _capacity);
a[0] = x;
a[1] = y;
a[2] = z;
}
Stack(const Stack& S) //拷贝构造函数
{
_capacity = S._capacity;
_top = S._top;
a = (int*)malloc(sizeof(int) * _capacity);
a[0] = S.a[0];
a[1] = S.a[1];
a[2] = S.a[2];
}
void Print()
{
cout << a << endl;
cout << a[0] << " " << a[1] << " " << a[2] << endl;
}
~Stack()//析构函数
{
free(a);
}
private:
int _capacity;
int _top;
int* a;
};
int main()
{
Stack s1(1, 2, 3);
Stack s2;
Stack s3(s1);
s1.Print();
s2.Print();
s3.Print();
return 0;
}
代码运行结果:
2.5 运算符重载
当运算符被用于类类型对象时,C++允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须调用对应的运算符重载,若没有对应的运算符重载,编译会报错。
运算符重载函数名由operator+运算符组成。运算符重载函数的参数个数和该运算符作用的运算对象数量一致。一元运算符有一个参数,二元运算符有两个参数,且二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。(这一点非常重要,牢记哦)
如果一个运算符重载函数是类的成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
运算符重载以后,其优先级和结合性与对应的内置类型运算符一致。
不能通过连接语法中没有的符号来创建新的操作符,比如:operator@。
“ .* ”、“ :: ”、“ sizeof ”、“ ?: ”、“ . ”,这五个运算符不能进行重载
重载运算符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator+(int x,int y)
重载++运算符时,有前置++与后置++,运算符重载函数名都是operator++,无法区分,而C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
重载<<和>>时,需要重载为全局函数。若重载为成员函数,this指针默认为第一个形参位置,第一个形参位置是左侧运算对象,调用时就变为:对象<<cout,不符合使用习惯和可读性。重载为全局函数,把ostream/istream放到第一个形参位置,第二个形参位置为类类型对象引用
若运算符重载函数在全局,如何访问对象的私有成员变量?
方法1. 成员放公有
方法2. 对象所在的类提供getxxx函数
方法3. 把运算符重载函数声明为该对象所在类的友元函数
方法4. 把运算符重载函数放在该对象所在类的域里,即使其成为成员函数
运算符重载使用示例:
#include <iostream>
using namespace std;
int arr[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
class Date
{
public:
Date(int year=1,int month=1,int day=1) //默认构造函数
{
_year = year;
_month = month;
_day = day;
}
int Getmday(int Y,int m)
{
if (m == 2)
{
if ((Y % 4 == 0 && Y % 100 != 0) || (Y % 400 == 0))
return arr[m - 1] + 1;
}
return arr[m - 1];
}
//+的重载
Date operator+(int x) //(Date* const this,int x)
{
Date a(*this);//拷贝构造,因为加法不影响两个加数
if (x >= 0)
{
a._day += x;
while (a._day > Getmday(a._year,a._month))
{
if (a._month == 12)
{
a._day -= Getmday(a._year, a._month);
++a._year;
a._month = 1;
}
else
{
a._day -= Getmday(a._year, a._month);
++a._month;
}
}
return a;
}
//x小于0暂不讨论
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date a(2024, 2, 9);
Date b(a + 100);
a.Print();
b.Print();
return 0;
}
运行结果:
2.6 赋值运算符重载
赋值运算符重载用于两个已经存在的对象的直接拷贝赋值,注意跟拷贝构造区分开,拷贝构造用于一个要被创建的对象对一个已存在对象进行拷贝以完成自己的初始化。
赋值运算符重载的特点:
1. 必须为成员函数。其参数建议为const修饰的该类类型的引用,如果参数为类类型,那么函数接收实参时还会调用拷贝构造,繁琐且无用。
2. 有返回值,建议返回值类型为该类类型的引用,引用返回可以提高效率,有返回值的目的是为了支持连续赋值场景。
3. 无显式实现时,编译器会自动生成一个默认赋值运算符重载,其会对内置类型成员进行浅拷贝,对自定义类型成员调用它所在的类的赋值重载函数。
4. 如果一个类显式实现了析构函数并释放了资源,那么该类同样需要显式写赋值运算符重载,否则就不需要。
演示示例:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //拷贝构造
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)//赋值运算符重载
{
if (this != &d) //检查是否自己给自己赋值的情况
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date a(2024, 9, 17);
Date b(1,1,1);
Date c(2,2,2);
c = b = a;
a.Print();
b.Print();
c.Print();
return 0;
}
代码运行结果 :
2.7 const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
实际上,const修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
例如:
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()const
{
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
该代码中Date的成员函数Print是被const修饰的,它隐含的this指针由Date* const this变为const Date* const this