1.类的定义
在C语言结构体中,只能定义变量,C++扩展了类的概念,能够在类定义函数;同时,struct仍然可以使用,但更常用class来表示类
1.1类中函数的两种定义方式
-
函数的声明和定义都在类中
class Date { public: void Init() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
-
函数的声明在.h文件中,定义在.cpp文件中,此时定义函数时必须指定类域
//Date.h class Date { public: void Init(); private: int _year; int _month; int _day; }; //Date.cpp void Date::Init() { cout << _year << "-" << _month << "-" << _day << endl; }
2.访问限定符和封装
C++有三大访问限定符,分别是public、protected、private
- 被public修饰的成员可以在类外直接被访问
- 被protected和private修饰的成员只能在类中访问
- struct的默认访问限定符是public,而class是private
通过访问限定符对成员进行修饰,想让外部访问的成员和不想让外部访问的成员,达到封装的效果
3. 类的作用域和实例化
3.1 作用域
在上篇文章中说到过,C++中有4种域:
- 局部域
- 全局域
- 类域
- 命名空间域
我们定义出来的类也是一种域,外部想要使用类中的成员,必须指定域
3.2 实例化
用类创建变量,叫做类的实例化;类的定义不会占用空间,只有实例化才会占用内存
3.3 类大小的计算
一个类中既有函数,又有变量,怎么计算一个类的大小呢?
//有成员函数和成员变量
class C1
{
void Fun1() {}
int _year;
};
//只有成员函数
class C2
{
void Fun2() {};
};
//空类
class C3
{};
int main()
{
cout << sizeof(C1) << endl;// 4
cout << sizeof(C2) << endl;// 1
cout << sizeof(C3) << endl;// 1
return 0;
}
实际上,如果想调用类中的函数,没有必要再复制函数的内容,因为每次调用的函数都是同一份;因此,类中函数的地址存放在公共代码区;计算类大小时,只计算成员变量所占的大小,并且要符合结构体内存对齐规则
注意:空类占1byte,很多人好奇为什么不是0byte,因为得知道这个类是存在的,只是什么都没有,如果是0byte,连内存都不占,我怎么知道它存在
说到结构体内存对齐,这里再提一个小问题:为什么要内存对齐?
效率方面,由于硬件在设计时,一次只能读取4byte或8byte,内存对齐更有利于拿取内存中的数据
更主要的原因,是因为硬件在读取时,只能在对应类型的整数倍处读取,不能在任意地方读取
class V
{
public:
void Fun()
{
cout << "Fun()" << endl;
}
};
int main()
{
V v1;
V* p1 = &v1;
p1->Fun();//打印Fun()
V* p2 = nullptr;
p2->Fun();//打印Fun()
return 0;
}
上面的代码中,p1访问类中的Fun()函数我们不难理解,但为什么p2明明是空指针,仍然能访问Fun()函数?
其实这个问题前面已经说过,是因为Fun函数在公共代码区;编译器首先会查找Fun()函数,既然它不在类中,那就没必要解引用了
4. this指针
4.1 this指针的引出
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2024, 1, 27);
d2.Init(2024, 2, 27);
d1.Print();// 打印2024-1-27
d2.Print();// 打印2024-2-27
return 0;
}
上面的代码中,调用Init()函数时,编译器怎么知道应该是去初始化d1,而不是d2呢?
实际上,编译器默认给每个成员函数增加了一个隐式指针参数,在调用时也会默认增加一个指针,该指针指向目标的地址,这就是this指针
4.2 this指针的特性
我们不能在形参或实参中显示this指针
我们可以在成员函数内部使用this指针,如果不对成员变量加上this指针,编译器会默认帮我们加上
void Print() { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; }
不能对this指针进行修改,因为this指针被const修饰
Date* const this
this指针在哪?
this指针在栈上,因为它实际上是一个形参,只不过传参和使用由编译器自动帮我们完成
class A1
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
class A2
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A1* p1 = nullptr;
A2* p2 = nullptr;
p1->Print();// 正常打印
p2->Print();// 程序崩溃
return 0;
}
出现两种情况,第一种前面已经解释过;第二种,因为调用Print()函数时,编译器默认帮我们传了p2,在Print()函数中,_a实际上是this->_a,因为this位nullptr,所以发生了空指针的解引用,程序崩溃
5. 默认成员函数
一个类中,其实默认含有一系列函数,这些函数如果程序员自己不定义,编译器会自动帮我们定义;有了这些函数,我们可以更加方便写代码,这就叫默认成员函数,主要有六个成员函数
5.1 构造函数
相信大家都犯过这样的错误,用C语言实现栈时,忘记初始化栈导致程序运行错误;为了防止未初始化函数就直接使用这种情况,祖师爷引入了构造函数的概念
5.1.2 概念
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2024, 1, 27);
d2.Init(2024, 2, 27);
d1.Print();
d2.Print();
return 0;
}
拿日期函数举例,我们发现每次调用Init()函数都要自己手动初始化,显得比较麻烦,能不能在类对象实例化时就直接给我们初始化了呢?
构造函数是一个特殊的成员函数,名字和类相同,创建类对象时由编译器自动调用,确保每一个成员变量都有一个初始值
5.1.3 特性
函数名与类名相同
无返回值
对象实例化时编译器自动调用
可以重载
class Date { public: //无参构造函数 Date(){} //带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1;// 调用无参构造函数 Date d2(2024, 1, 27);// 调用带参构造函数 return 0; } //注意:如果是调用无参构造函数,不能写成【Date d1()】,因为这就和函数声明冲突了,编译器无法识别这到底是函数声明,还是调用无参构造函数
如果类中没有显式定义构造函数,那么编译器会自动生成一个无参构造函数;而如果类中有显式构造函数,编译器就不会生成默认构造函数
默认构造函数不会对内置类型进行初始化;而如果有自定义类型,会去调用自定义类型的构造函数
class Date { private: int _year; int _month; int _day; }; int main() { Date d1; //实例化之后,发现d1中的成员变量是随机值,也就是说默认构造函数没有做任何事情 return 0; }
class Week { public: Week() { _week = 0; } private: int _week; }; class Date { private: int _year; int _month; int _day; Week _date; }; int main() { Date d1; return 0; } //对于_date,编译器会去调用类Week中的构造函数Week()
不管怎么看,构造函数的这个特性都不太对劲,我本来想让你帮我完成初始化的工作,结果你什么都没做,有点不太合理;因此C++11增加了新的规则,可以在成员变量声明时给默认值
class Date { private: int _year = 1; int _month = 1; int _day = 1; };
无参构造函数、全缺省构造函数、编译器生成的构造参数,都可以叫做默认构造参数,且三个只能存在一个
class Date { public: Date() { _year = 2024; _month = 1; _day = 27; } Date(int year = 2024, int month = 1, int day = 27) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; return 0; } //编译器会报错,因为实例化d1时,没有指定数据,表明是调用默认构造函数,而类中有两个默认构造函数,编译器无法区分,因此会报错
5.2 析构函数
5.2.1 概念
析构函数是类中特殊的函数,当对象销毁时,他会自动帮我们完成内存清理的工作
5.2.2 特性
析构函数的函数名是在类名前加上~
无参数无返回值
一个类只能有一个析构函数,若没有显式定义,则调用默认析构函数
析构函数不能重载
对象生命周期结束时,自动调用析构函数
class Stack { public: Stack(int capacity = 4) { _arr = (int*)malloc(sizeof(int) * capacity); if (_arr == NULL) { perror("mallco fail"); return; } _size = 0; _capacity = capacity; } void Push(int x) { //CheckCapacity(); _arr[_size++] = x; } void Pop() { assert(_size > 0); _size--; } void Print() { for (int i = 0; i < _size; i++) { printf("%d\n", _arr[i]); } } ~Stack() { free(_arr); _arr = NULL; _size = _capacity = 0; } private: int* _arr; int _size; int _capacity; }; int main() { Stack s; s.Push(1); s.Push(2); s.Print(); s.Pop(); s.Print(); return 0; }
如果没有显式析构函数,编译器调用默认析构函数;默认析构函数会调用自定义类型成员中的析构函数
class Time { public: ~Time() { cout << "Time()" << endl; } private: int _value = 1; }; class Date { private: int _year = 2024; int _month = 1; int _day = 27; Time _t; }; int main() { Date d1; return 0; } //Date类中的内置类型_year、_month、_day,在d1销毁时自己释放,不需要析构函数对其进行资源释放 //对于自定义类型_t,d1在销毁前需要对其进行检查,是否进行资源释放,但main函数中不能直接访问Time类 //因此Date类中编译器生成的默认析构函数会去调用Time类中的析构函数
如果类中没有申请资源时,析构函数可以不写,由编译器自动生成;但如果有申请资源,需要程序员自己写好对应的析构函数
程序结束时,析构函数的调用规则满足后定义的先析构:局部变量–>局部静态变量–>全局或全局静态变量
class Date { public: Date(int year = 1) { _year = year; } ~Date() { cout << "Date()->" << _year << endl; } private: int _year; int _month = 1; int _day = 1; }; Date d4(4); Date d5(5); static Date d6(6); void fun() { Date d7(7); static Date d9(9); Date d8(8); } int main() { fun(); Date d1(1); Date d2(2); static Date d3(3); return 0; } //正确的析构顺序:8->7->2->1->3->9->6->5->4
5.3 拷贝构造函数
5.3.1 概念
拷贝构造函数是一种成员函数,完成一个对象的拷贝工作
5.3.2 特性
拷贝构造函数是构造函数的一个重载形式
拷贝构造函数只能有一个形参(用const修饰),且类型必须是类名的引用,如果类型是类名,会引发无穷递归的问题
C++中规定,对类进行传值传参,首先得调用该类的拷贝构造函数
由此会一直调用下去,所以拷贝构造函数的参数类型必须是类名引用类型;同时,为了防止修改掉原来的数据,最好加上const修饰
如果拷贝构造函数没有显示定义,则编译器会默认生成拷贝构造函数;与构造函数不同,默认生成的拷贝构造函数对于内置类型会按字节顺序拷贝数据,对于自定义类型,会去调用它的拷贝构造函数;按字节顺序拷贝数据叫做浅拷贝
class Time { public: Time() = default; // Date d2(d1) Time(const Time& t) { _hour = t._hour; _minute = t._minute; _second = t._second; } private: int _hour = 1; int _minute = 1; int _second = 1; }; class Date { private: int _year = 2024; int _month = 2; int _day = 26; Time _t; }; int main() { Date d1; Date d2(d1); return 0; }
拷贝构造函数与之前的成员函数不同,它会帮我们完成拷贝,那么是不是说就不需要我们写拷贝构造函数了呢?对于上面的日期类,好像是这样的,但如果换成栈类呢?
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
//CheckCapacity();
_array[_top] = data;
_top++;
}
~Stack()
{
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
private:
DataType* _array;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2(s1);
return 0;
}
// 执行这段代码,我们发现程序崩了
经过调试,我们发现s2确实完成了s1的拷贝
但仔细分析一下,s1中的array是我们自己申请的空间,程序在结束前,会去调用析构函数,释放掉s1中array的空间,而此时s2仍指向被释放掉的空间,这就出现了野指针的问题;下次释放s2时,对同一块空间释放了两次,因此程序会崩掉
由此总结,对于不需要申请空间的类,拷贝构造函数确实可以不写;而如果有申请空间,需要我们自己写好拷贝构造函数;而对于申请空间的拷贝我们叫做深拷贝
//需要我们自行完成拷贝构造函数
Stack(const Stack& s)
{
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, s._array, sizeof(DataType) * s._top);
_top = s._top;
_capacity = s._capacity;
}
6. 运算符重载
为了代码的可读性,C++提供了对运算符赋予新的涵义的操作,也叫运算符重载
对于日期的比较,按照之前的思路写代码:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool CompareEqual(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
bool CompareMore(const Date& d2)
{
if (_year > d2._year)
return true;
else if (_year == d2._year)
{
if (_month > d2._month)
return true;
else if (_month == d2._month)
{
if (_day > d2._day)
return true;
}
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 2, 28);
Date d2(2024, 2, 26);
cout << d1.CompareEqual(d2) << endl;
cout << d1.CompareMore(d2) << endl;
return 0;
}
这样写能完成我们想要的结果,但在函数的命名上有些问题;有时我们并不能通过函数名就知道该函数是做什么的;我希望通过函数名就能知道该函数比较的是什么;C++能够使用operator+运算符对函数命名
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
bool operator>(const Date& d2)
{
if (_year > d2._year)
return true;
else if (_year == d2._year)
{
if (_month > d2._month)
return true;
else if (_month == d2._month)
{
if (_day > d2._day)
return true;
}
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 2, 28);
Date d2(2024, 2, 26);
//cout << d1.operator==(d2) << endl;
//cout << d1.operator>(d2) << endl;
cout << (d1 == d2) << endl;
cout << (d1 > d2) << endl;
return 0;
}
函数比较的是什么;C++能够使用operator+运算符对函数命名
```C++
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
bool operator>(const Date& d2)
{
if (_year > d2._year)
return true;
else if (_year == d2._year)
{
if (_month > d2._month)
return true;
else if (_month == d2._month)
{
if (_day > d2._day)
return true;
}
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 2, 28);
Date d2(2024, 2, 26);
//cout << d1.operator==(d2) << endl;
//cout << d1.operator>(d2) << endl;
cout << (d1 == d2) << endl;
cout << (d1 > d2) << endl;
return 0;
}