[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了
Posted 哦哦呵呵
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++] C++面向对象,看了它,你和本贾尼就只有一步之遥了相关的知识,希望对你有一定的参考价值。
我们知道C++相对于C语言最大的提升便是加入了面向对象这一特性,本文将对面向对象的所有特性进行说明。并且以下文章中,所有示例及结果的实验环境为 win10-64,实验环境为为visual stdio2019
1. 什么是面向对象?
在学习C语言阶段,我们编写程序关心的是过程,分析求解问题的步骤,通过函数的调用一步步解决问题,这是C语言面向过程的思想。
现在C++语言引入了面向对象的思想,我们在编写程序时,不再单单只关注过程,而是关注我们操作的这个对象都有什么功能,都可以完成一些什么事情,依靠对象之间的交互完成一些功能的实现。
- 面向对象是一种对现实世界理解和抽象的方法,通过两部分进行描述对象,属性+功能,表示一个具体的对象。
- 面向过程 是一种 以过程为中心 的编程思想。以该功能的实现过程为主要思想。
面向过程的举例(以把大象放入冰箱为例)
面向对象举例
通过上述的比较,在简单的程序中,面向对象更方便,但是如果我们在后期继续扩展,则面向对象的优势会进一步的显现出来。面向对象的继承、封装、多态的属性会在接下来的文章中详细介绍。
这篇文章很详细的阐述了面向对象的优势所在,点击:面向对象的优势 (转自leetcode)查看。
1.1 类和对象的引入
在使用面向对象解决问题时,首先需要有该对象,所以通过什么手段进行描述。
对象的组成:
1.属性 -->该对象的具体属性,比如上述冰箱的属性就会具有 门、容量、冷冻…
2.功能 -->该对象能完成的事情,比如冰箱关门、开门…
- 类:用来描述对象,是一种自定义类型
- 对象:是类的实体,有属性和行为
2. 什么是类及类相关知识
2.1 C语言与C++结构体的区别
1.在C语言中,结构体内部只能定义变量,而不能定义函数。所以C语言中的结构体没有面向对象的属性,将C语言结构体定义出来的变量就叫结构体变量。
struct Stu
{
int id;
char* name;
int getName() // error
{
return this->id;
}
};
2.而在C++中,结构体内部不仅可以定义变量,还可以定义函数,并且可以通过成员访问运算符访问成员函数与成员变量,所以它具有面向对象的属性,在C++中我们把使用struct
定义出来的变量叫做对象
。
struct Stu
{
int id;
char* name;
int getName() // ok
{
return this->id;
}
};
// 以下操作都可以正常使用
cout << stu.id << stu.name << stu.getName() << endl;
2.2 class关键字
虽然在C++中使用struct关键字可以定义对象,但是在C++中一般不会去使用struct
去定义对象,而是使用class
关键字进行对象的定义。
class className
{
// 类体:成员函数与成员变量(对象的属性与方法)
};
2.3 类的两种定义形式
2.3.1 声明定义一起实现
使用这种方式定义,编译器可能会将其当作内敛函数进行处理。
class Student
{
private:
void showMessage()
{
cout << id << name << gender << endl;
}
public:
int id;
char name[32];
char gender[2];
};
2.3.2 声明定义分文件处理
推荐使用该方式。
// Student.h
class Student
{
private:
void showMessage();
public:
int id;
char name[32];
char gender[2];
};
// Student.cpp
#include "Student.h"
// 通过域运算符 '::' 进行成员函数的实现
void Student::showMessage()
{
cout << id << name << gender << endl;
}
2.4 类的访问限定符与封装
2.4.1 封装
将数据和操作数据的方法进行有机结合,访问权限限定符
用来隐藏对象的属性和细节,仅对外公开接口来和对象进行交互。
封装的本质其实是管理:将我们不想让外界知道的细节进行隐藏,开放一些公有的成员函数,对成员进行合理的访问。防止外界通过不正当方式破坏内部数据。
2.4.2 访问限定符
C++通过访问限定符进行类的封装,使用访问限定符来决定将哪些接口提供给外界访问。
- public(公有): 修饰的成员在类外可以被直接访问(类中的成员函数一般通过这种方式开放)
- private(私有): 修饰的成员在类外不能直接被访问
- protected(保护): protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private
注意:
- 访问权限作用域从该访问限定符出现的位置开始知道下一个访问限定符出现时为止
- class的默认访问权限为private,struct的访问权限为public(因为C语言中没有私有权限,所以要兼容C语言,权限设置为public)
2.4 类的作用域
结论:一个类就是一个作用域,在访问类中的成员是需要使用域运算符::
。
class Student
{
private:
void showSMessage();
public:
int id;
char name[32];
char gender[2];
};
class Teacher
{
private:
void showTMessage();
public:
int id;
char name[32];
char gender[2];
};
// 通过域运算符 '::' 指定该成员属于哪个作用域
void Student::showSMessage()
{
cout << id << name << gender << endl;
}
void Teacher::showTMessage()
{
cout << id << name << gender << endl;
}
2.5 类的实例化
使用类创建对象的过程,就成为类的实例化
- 类只是一个模型,描述了该模型,属性域方法,定义出一个类并没有分配实际的内存空间来存储。
- 一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类的成员变量。
// 定义类的代码就不看了,直接看如何实例化
int main()
{
// 实例化一个对象
Student stu1;
// 通过成员访问运算符访问类的成员
stu1.id = 10;
stu1.name = "张三";
stu1.gender = "男";
stu1.showSMessage();
Student stu2, stu3; // 还可以实例化多个对象
}
2.5 类如何在内存中存储(通过计算对象的大小观察)
2.5.1 对类存储模型的三种猜想
1. 对象包含所有的成员
每新建一个对象,该对象中存储该类中的所有成员。每一个对象都是如此。
稍微思考一番,就觉得这种方法不可能,为什么?
如果每个对象都将类中的成员及成员函数存储一份,那么对象每个对象变得异常的大。如果该类有多个对象,每个对象的数据单独存储没问题,但成员函数中的代码都是相同的,如果每次都保存相同代码,造成的空间浪费是巨大的。
2. 成员函数放在类外,通过指针指向函数位置
这种存储方式貌似可行,解决了代码重用问题,并且缩小了空间。那我么写个程序测试以下,看是否与我们猜测一致。
#include <iostream>
using namespace std;
class Student
{
public:
void showMessage()
{
cout << id << "-" << name << "-" << gender << endl;
}
char* getName()
{
return name;
}
//private:
int id;
char name[32];
char gender[2];
};
int main()
{
Student stu;
stu.id = 10;
strcpy(stu.name, "张三");
strcpy(stu.gender, "男");
stu.getName();
stu.showMessage();
cout << "sizeof(stu): " << sizeof(stu) << endl;
cout << "sizeof(Student): " << sizeof(Student) << endl;
return 0;
}
上述程序,如果按照函数只存储函数指针的方式我们猜测大小应该为
44
或者48
。但是!!!,结果是40
,至此我们已经得出了结论。
3. 对象中只包含了成员变量
通过以上两种猜想,我们可以确定对象的存储模型只存储了成员变量,那么在使用时对象如何去寻找成员函数。编译器在编译时就会确定成员函数存储在何种位置,不需要指定存储位置。
2.5.2 给定一个空类,它的大小是多少
class Test
{};
由结果可知,空类所占大小为1个字节
假设空类大小为0个字节,使用空类创建了多个对象,但这些对象都没有空间,则他们在内存中就无法标识,并且在一个地方存储。就无法正确表示出这个对象。所以一定要由一个字节进行占位。
2.5.3 结论
对象的存储方式为,对象只包含成员变量,并不包含成员函数。并且按照内存对齐
的方式进行存储。大小计算也与结构体的计算方式相同。空类占1个字节。
3. this指针
3.1 问题提出
1.在类中,为什么可以把成员变量放在成员函数下方定义,并且可以正常使用?
2.多个对象调用一个函数,该函数怎么区分对哪个对象操作?
3.2 this指针的作用
在上述的问题2中,如何多个对象调用一个函数,如何区分是哪个对象调用?
通过this指针解决该问题。即在编译期间C++编译器,对每个
非静态的成员函数
增加了一个隐藏的指针参数,让该指针指向当前对象,在函数体中所有的成员变量操作,都是通过这个指针去访问。只不过不需要用户对该指针进行传递,编译器自动完成。
在成员函数中可以通过this->XXX
访问成员变量
class Student
{
public:
Student(int id, const char name[], const char gender[])
{
_id = id;
strcpy(_name, name);
strcpy(_gender, gender);
}
void showMessage()
{
cout << "this address: " << this << endl;
cout << _id << "-" << _name << "-" << _gender << endl;
// 也可以通过这样访问
cout << this->_id << this->_name << endl;
}
private:
int _id;
char _name[32];
char _gender[2];
};
int main()
{
Student stu1(1, "张三", "男");
cout << "&stu1 = " << &stu1 << endl;
stu1.showMessage();
cout << endl;
Student stu2(2, "李四", "女");
cout << "&stu2 = " << &stu2 << endl;
stu2.showMessage();
return 0;
}
结果:
3.3 this指针的特性
- this指针的类型:
类类型* const
,因为this指针的指向不能被修改- 只能在成员函数内部使用
- this指针本质上是成员函数的形参,是对象调用参数时,将对象地址作为实参传递给this形参,所以对象不存储this指针
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过寄存器自动传递,不需要用户传递。
void showMessage(/*Student* this*/) { cout << "this address: " << this << endl; cout << _id << "-" << _name << "-" << _gender << endl; // 也可以通过这样访问 cout << this->_id << this->_name << endl; }
4. 默认的成员函数
4.1 类的6个默认成员函数
如果一个类中什么成员都没有,则称该类空空类。但是空类中真的什么都没有吗?在我们声明一个类时,不管我们是否向类中添加成员,都会生成下列的6个默认的成员函数。
- 初始化和清理:
1. 构造函数:完成成员变量的初始化
2. 析构函数:完成成员变量的清理- 拷贝复制:
1. 拷贝构造函数:使用同类对象初始化创建对象
2. 赋值运算符重载:把一个对象赋值给另一个对象- 取地址重载:
1. 普通对象的取地址
2. const对象取地址
4.2 构造函数
4.2.1 作用及用法
在类中,由于其中的成员变量具有封装的特性,所以无法直接对其中的成员赋值,所以要通过设置公有方法进行对私有属性的改变,这样很麻烦。在对象创建时就把对应的信息设置进去。
所以,构造函数就出现了,构造函数较特殊,函数名与类名相同,并且无返回值,创建类类型对象时由编译器自动调用,并且在该对象的声明周期中只会调用一次。如果没有定义构造函数,则编译器会自动生成一个无参的构造函数。
没有构造函数的对成员进行设值 每次新建对象都这样这样操作
class Stu
{
public:
void SetId(int _id)
{
id = _id;
}
void SetAge(int _age)
{
age = _age;
}
private:
int id;
int age;
};
int main()
{
// 给对象设置值
Stu s1;
s1.SetAge(10);
s1.SetId(1111);
return 0;
}
利用构造函数设值 显然方便了许多
class Stu
{
public:
Stu(int _id, int _age)
{
id = _id;
age = _age;
}
private:
int id;
int age;
};
int main()
{
// 新建对象时设值
Stu s1(10, 123);
Stu s2(11, 124);
Stu s3(13, 125);
return 0;
}
4.2.2 特性
1.构造函数的重载
注意:一旦自己改写了,构造函数,则编译器不会再生成默认的构造函数
如果有时候不需要对新建的对象设值,那么上面的方式就达不到我们的要求,但是构造函数的另一特性又出现了,可以对构造函数进行重载,满足编程时的不同要求。
class Stu
{
public:
// 两个参数的构造函数
Stu(int _id, int _age)
{
id = _id;
age = _age;
}
// 无参的构造函数
Stu() {}
// 一个参数的构造函数
Stu(int _id)
{
id = _id;
}
private:
int id;
int age;
};
int main()
{
// 利用重载创建不同对象
Stu s1;
Stu s2(1, 123);
Stu s3(1);
return 0;
}
2. 用户如果定义构造函数则不会自动生成无参构造函数
class Stu
{
public:
// 两个参数的构造函数
Stu(int _id, int _age)
{
id = _id;
age = _age;
}
private:
int id;
int age;
};
int main()
{
Stu s1; // err 因为找不到无参的构造函数
Stu s2(1, 123);
return 0;
}
3. 无参构造函数和全缺省的构造函数都是默认构造函数,只能有一个
class Stu
{
public:
// 全缺省构造函数
Stu(int _id = 1, int _age = 10)
{
id = _id;
age = _age;
}
// 无参构造函数
Stu() {}
private:
int id;
int age;
};
int main()
{
Stu s1; // 产生了二义性
Stu s2(1, 123);
return 0;
}
4. 如果类嵌套类,则在生成类对象时,会自动调用嵌套类的无参构造函数
5. 在构造函数中是对成员的赋值,不是对成员的初始化
4.3 初始化列表
在上述构造函数内部,我们给成员变量赋值,在那一部分,不是对成员变量的初始化,因为初始化只有一次,而函数体内部的赋值可以有多次。如果对成员变量进行初始化,就利用到了现在的初始化列表。
4.3.1 初始化列表的使用
class Stu
{
public:
Stu(int _id = 1, int _age = 10)
: id(_id) // 初始化列表以 ':' 开始
, age(_age) // 以 ',' 进行分隔
{}
private:
int id;
int age;
};
4.3.1 注意事项
1.类中包含以下成员则必须在初始化列表位置进行初始化
1.1 const成员变量
因为const成员变量无法在声明时进行赋值,但是const具有不可修改的特性,但是不能没有初始值,所以就要在初始化列表的位置进行初始化。
class Stu
{
public:
Stu(int _id = 1, int _age = 10)
: id(_id)
, age(_age)
, status(1) // 对const成员变量进行初始化
{}
private:
int id;
int age;
// const成员变量
const int status;
};
1.2 引用成员变量
原因上同
class Stu
{
public:
Stu(int _id = 1, int _age = 10)
: id(_id)
, age(_age)
, ref(_age)
{}
private:
int id;
int age;
//
int& ref;
};
int main()
{
//Stu s1; // 产生了二义性
Stu s2(1, 123);
return 0;
}
1.3 没有默认构造函数的自定义类型
全缺省和无参构造函数都被称为默认构造函数
如果自定义类型有默认的构造函数,再创建母对象时,会调用自定义类型的无参构造函数。若没有默认的构造函数则必须要给类型传参数,否则无法成功建立对象,所以要在初始化列表的位置进行自定义类型的初始化。
class Test
{
public:
Test(int _score)
: score(_score)
{}
private:
int score;
};
class Stu
{
public:
Stu(Test _test, int _id = 1, int _age = 10)
: id(_id)
, age(_age)
, test(_test) // 自定义类型初始化
{}
private:
int id;
int age;
//自定义类型的成员
Test test;
};
1.4 成员变量的初始化顺序,是在类中的声明顺序,与初始化列表顺序无关
class Test
{
public:
// 初始化列表的初始化顺序:
// b -> c -> a
// 因为先用b初始化的a,但a中为随机值,所以b中也是随机值
Test(int _a = 1, int _b = 2, int _c = 3)
: a(_a)
, b(a)
, c(_c)
{}
void PrintRes()
{
cout << a << " " << b << " " << c << endl;
}
private:
int b;
int c;
int a; // 注意这里 a 的位置
};
int main()
{
Test test;
test.PrintRes();
return 0;
}
4.4 析构函数
4.4.1 功能
析构函数的功能与构造函数功能恰恰相反,析构函数不是完成对象的销毁,而是完成对象中资源的清理的工作,对象在销毁时,会自动调用对象的析构函数,完成对象中资源的清理。如果在构造对象时,没有申请资源,则可以不去写析构函数。
class Stu
{
public:
Stu(int _id, const char* _name)
: C++教程