文章目录
- [1. 面向过程与面向对象](#1. 面向过程与面向对象)
- [2. 类(class)](#2. 类(class))
- [3. 访问限定符](#3. 访问限定符)
- [4. 类的实例化](#4. 类的实例化)
- [5. this指针](#5. this指针)
- [6. 默认成员函数](#6. 默认成员函数)
-
- [6.1 构造函数](#6.1 构造函数)
- [6.2 析构函数](#6.2 析构函数)
- [6.3 拷贝构造函数](#6.3 拷贝构造函数)
1. 面向过程与面向对象
C语言是面向过程(procedure-oriented)的语言,分析出求解问题的步骤,通过函数调用逐步解决问题。以洗衣服举例,需要很多个步骤:
C++是面向对象(object-oriented)的语言,将一件事情拆分成不同的对象,靠对象之间的交互完成。洗衣服这件事可以拆分成三个对象:人、衣服和洗衣粉,可能也有洗衣机,洗衣服这个过程是由这三个或四个对象之间交互完成的。
2. 类(class)
C语言结构体中只能定义变量,在C++中结构体内不仅可以定义变量,也可以定义函数。
cpp
struct Person {
char name[20];
int age;
char gender;
int height;
int weight;
char introduction[100];
void showInfo() {
cout << name << " - " << age << " - " << gender << endl;
}
void sleep() {
}
void washCloth() {
}
void readBook() {
}
void work() {
}
void study() {
}
};
可以这么做是因为C++需要兼容C,事实上C++更喜欢用class关键字表示一个类:
cpp
class Person
{
// 类体由变量和函数组成
};
类中的变量称为类的属性或成员变量 ,类中的函数称为类的方法或成员函数 。可以将成员的声明和定义一起放在类中 ,如果成员函数放在类中定义,编译器可能会将其当成内联函数处理;也可以将类成员的声明和定义分开,类中的成员变量和成员函数的声明放在.h头文件,成员函数的定义放在.cpp文件。
类的作用域
类定义了一个新的作用域,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
cpp
class Person {
char name[20];
int age;
char gender;
int height;
int weight;
char introduction[100];
void showInfo();
void sleep();
void washCloth();
void readBook();
void work();
void study();
};
void Person::showInfo() {
cout << name << " - " << age << " - " << gender << endl;
}
3. 访问限定符
访问限定符用于确定类成员的访问权限:
- public:被public修饰的成员在类外可以被访问;
- protected:被protected修饰的成员在类外不能被访问,但可以在继承的子类中被访问;
- private:被private修饰的成员在类外不能被访问。
class关键字定义类的默认访问权限是private,这是因为面向对象三大特性之一的封装。struct关键字定义类的默认访问权限是public,因为需要兼容C。
封装
面向对象的三大特性:封装、继承、多态。封装是将数据和操作数据的方法进行结合,通过访问权限来隐藏对象内部的属性和实现细节,控制哪些函数可以在类外部直接被使用,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互。对于计算机使用者而言,不用关心内部核心部件,主板上线路是如何布局的,CPU内部是如何设计的。
cpp
class Person {
private:
char name[20];
int age;
char gender;
int height;
int weight;
char introduction[100];
public:
void showInfo();
void sleep();
void washCloth();
void readBook();
void work();
void study();
};
void Person::showInfo() {
cout << name << " - " << age << " - " << gender << endl;
}
4. 类的实例化
用类类型创建对象的过程,称为类的实例化。类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类成员变量。
cpp
int main() {
Person p1;
Person p2;
Person p3;
}
计算对象的内存大小与计算结构体的大小一致,可以看这篇文章计算结构体的大小了解,简单地说就是按计算结构体大小的方式来计算类成员变量的大小。成员函数不会包括在内,因为成员函数是n个对象共用的,存放在公共代码区。另外空类比较特殊,它也有大小,占用1个字节空间。
5. this指针
this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参,因此this指针并不存放在对象中,而是在栈中(可能也在寄存器中,取决于编译器)。
this指针的类型:类的类型* const,只能在"成员函数"的内部使用。this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要程序员手动传递。
下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
cpp
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
C.正常运行,表面存在空指针问题,但成员函数中并没有使用其它成员,仅仅只是打印一个字符串,不会导致空指针访问。
cpp
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
B.运行崩溃,成员函数中使用了成员变量,实际上是this指针访问的成员,造成了空指针。
6. 默认成员函数
如果一个类中什么成员都没有,简称为空类。空类并不是什么都没有,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
1. 构造函数:初始化对象;
2. 析构函数:清理对象中申请的动态内存;
3. 拷贝构造:使用同类对象创建另一个对象;
4. 赋值重载:把一个对象赋值给另一个对象;
-
普通对象取地址重载;
-
const对象取地址重载。
后面两个取地址重载很少用。
6.1 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,并不是开空间创建对象,而是完成对象的初始化工作。
cpp
class Date {
private:
int year;
int month;
int day;
public:
Date() { // 无参构造
}
Date(int y, int m, int d) {
year = y;
month = m;
day = d;
}
};
int main() {
Date date1;
Date date2(2024, 3, 11);
return 0;
}
如果通过无参构造函数创建对象,后面不用跟括号,否则就成了函数声明。
构造函数特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数、全缺省的构造函数、编译器默认生成的构造函数都称为默认构造函数,在一个类中这三个默认构造函数只能存在其中一个,因为这三个都可以不用传参,如果同时存在会产生调用歧义。
cpp
class Date {
private:
int year;
int month;
int day;
public:
Date() { // 与下面全缺省的构造函数存在冲突
}
Date(int y = 2024, int m = 2, int d = 22) {
year = y;
month = m;
day = d;
}
};
- 编译器生成默认的构造函数,会对类中其它自定类型成员调用的它的默认构造
函数。比如Date类中如果包含一个Time类成员:
cpp
class Time {
private:
int hour;
int minute;
int second;
public:
Time() {
cout << "Time()" << endl;
}
};
class Date {
private:
int year;
int month;
int day;
Time time;
public:
Date(int y = 2024, int m = 2, int d = 22) {
cout << "Date(int, int, int)" << endl;
year = y;
month = m;
day = d;
}
};
int main() {
Date date;
return 0;
}
由于默认生成的构造函数不会进行有效的初始化,给的是随机值,所以C++11开始可以给内置类型(int/char/double等)成员在类中声明时给默认值,如果没有指定初始化则初始化成默认值。
cpp
class Date
{
private:
int _year = 1970;
int _month = 1;
int _day = 1;
Time _time;
};
6.2 析构函数
析构函数不是完成对对象本身的销毁,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数和无返回值类型。
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
- 析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
cpp
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("Stack malloc failed.");
return;
}
_capacity = capacity;
_size = 0;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
- 编译器生成的默认析构函数,对自定义类型的成员调用它的析构函数。
cpp
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 1970;
int _month = 1;
int _day = 1;
Time _time;
};
int main()
{
Date d;
return 0;
}
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。有资源申请时(malloc),一定要写,否则会造成资源泄漏。
6.3 拷贝构造函数
拷贝构造函数创建一个与已存在对象一样值的新对象。参数只有单个形参,该形参是对本类类型对象的引用,且一般常用const修饰,在用已存在的类型对象创建新对象时由编译器自动调用。
拷贝构造函数特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用 ,使用传值方式的话编译器直接报错,因为会引发无穷递归调用。这是错误写法,传值会引发无穷递归:
cpp
Date(const Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
原因如下:
cpp
void Test1(const Date d) {
}
void Test2(const Date& d) {
}
int main() {
Date date1;
Test1(date1); // 调用Test1传值会首先调用拷贝构造
Test2(date1); // 传引用不会去调用拷贝构造
}
所以说拷贝构造的形参不使用引用,会引发无穷递归:
初始化date2时传值而不传引用,调用了一层拷贝构造后,拷贝构造本身继续传值继续调用拷贝构造引发无穷递归,现在这样写的话vs直接爆红,编译都不给编译。
- 若未显式定义拷贝构造,默认的拷贝构造函数按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝) 。如果类中有需要申请内存资源的成员(需要malloc的成员),默认的拷贝构造无法完成拷贝,需要自己显示定义完成深拷贝。比如:
cpp
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("Stack malloc failed.");
return;
}
_capacity = capacity;
_size = 0;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
使用默认的拷贝构造传入stack1初始化stack2,stack2的array仅仅只是把stack1的array地址值拷贝过来了,意味着共用同一块内存。
显示定义拷贝构造为深拷贝的正确写法:
cpp
Stack(const Stack& rStack) {
_capacity = rStack._capacity;
_size = rStack._size;
_array = (DataType*)malloc(sizeof(DataType) * _capacity);
memcpy(_array, rStack._array, _size);
//for (int i = 0; i < _size; ++i) {
// _array[i] = rStack._array[i];
//}
}
为了提高程序效率,一般对象传参时,尽量使用引用类型;函数返回值根据实际场景,能用引用尽量使用引用。