文章目录
- 1. 面向过程与面向对象
- 2. 类(class)
- 类的作用域
- 3. 访问限定符
- 封装
- 4. 类的实例化
- 5. this指针
- 6. 默认成员函数
- 6.1 构造函数
- 6.2 析构函数
- 6.3 拷贝构造函数
1. 面向过程与面向对象
C语言是面向过程(procedure-oriented)的语言,分析出求解问题的步骤,通过函数调用逐步解决问题。以洗衣服举例,需要很多个步骤:
C++是面向对象(object-oriented)的语言,将一件事情拆分成不同的对象,靠对象之间的交互完成。洗衣服这件事可以拆分成三个对象:人、衣服和洗衣粉,可能也有洗衣机,洗衣服这个过程是由这三个或四个对象之间交互完成的。
2. 类(class)
C语言结构体中只能定义变量,在C++中结构体内不仅可以定义变量,也可以定义函数。
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关键字表示一个类:
class Person
{// 类体由变量和函数组成
};
类中的变量称为类的属性或成员变量,类中的函数称为类的方法或成员函数。可以将成员的声明和定义一起放在类中,如果成员函数放在类中定义,编译器可能会将其当成内联函数处理;也可以将类成员的声明和定义分开,类中的成员变量和成员函数的声明放在.h头文件,成员函数的定义放在.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内部是如何设计的。
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. 类的实例化
用类类型创建对象的过程,称为类的实例化。类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类成员变量。
int main() {Person p1;Person p2;Person p3;
}
计算对象的内存大小与计算结构体的大小一致,可以看这篇文章计算结构体的大小了解,简单地说就是按计算结构体大小的方式来计算类成员变量的大小。成员函数不会包括在内,因为成员函数是n个对象共用的,存放在公共代码区。另外空类比较特殊,它也有大小,占用1个字节空间。
5. this指针
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参,因此this指针并不存放在对象中,而是在栈中(可能也在寄存器中,取决于编译器)。
this指针的类型:类的类型* const,只能在“成员函数”的内部使用。this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要程序员手动传递。
下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0;
}
C.正常运行,表面存在空指针问题,但成员函数中并没有使用其它成员,仅仅只是打印一个字符串,不会导致空指针访问。
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. 赋值重载:把一个对象赋值给另一个对象;
5. 普通对象取地址重载;
6. const对象取地址重载。
后面两个取地址重载很少用。
6.1 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,并不是开空间创建对象,而是完成对象的初始化工作。
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++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数、全缺省的构造函数、编译器默认生成的构造函数都称为默认构造函数,在一个类中这三个默认构造函数只能存在其中一个,因为这三个都可以不用传参,如果同时存在会产生调用歧义。
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类成员:
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等)成员在类中声明时给默认值,如果没有指定初始化则初始化成默认值。
class Date
{
private:int _year = 1970;int _month = 1;int _day = 1;Time _time;
};
6.2 析构函数
析构函数不是完成对对象本身的销毁,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数和无返回值类型。
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
- 析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
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;
};
- 编译器生成的默认析构函数,对自定义类型的成员调用它的析构函数。
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修饰,在用已存在的类型对象创建新对象时由编译器自动调用。
拷贝构造函数特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
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;
};
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式的话编译器直接报错,因为会引发无穷递归调用。这是错误写法,传值会引发无穷递归:
Date(const Date d) {_year = d._year;_month = d._month;_day = d._day;}
原因如下:
void Test1(const Date d) {}
void Test2(const Date& d) {}
int main() {Date date1;Test1(date1); // 调用Test1传值会首先调用拷贝构造Test2(date1); // 传引用不会去调用拷贝构造
}
所以说拷贝构造的形参不使用引用,会引发无穷递归:
初始化date2时传值而不传引用,调用了一层拷贝构造后,拷贝构造本身继续传值继续调用拷贝构造引发无穷递归,现在这样写的话vs直接爆红,编译都不给编译。
- 若未显式定义拷贝构造,默认的拷贝构造函数按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)。如果类中有需要申请内存资源的成员(需要malloc的成员),默认的拷贝构造无法完成拷贝,需要自己显示定义完成深拷贝。比如:
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地址值拷贝过来了,意味着共用同一块内存。
显示定义拷贝构造为深拷贝的正确写法:
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];//}}
为了提高程序效率,一般对象传参时,尽量使用引用类型;函数返回值根据实际场景,能用引用尽量使用引用。