面向过程与面向对象
上一次我们就说到,C语言是一门面向过程的编程语言,面向过程的特点是注重过程,分析要解决的问题,求出步骤,通过函数调用逐步解决问题。而C++是一门面向对象的编程语言,面向对象的特点是注重对象,将一件事拆解成不同的对象,靠对象之间的交互完成。
举个例子:
狗吃骨头,如果使用面向过程的思想将狗吃骨头划分为:狗张嘴——吃骨头——骨头没了。
按照面向对象的思想就会把狗吃骨头分为好几个对象,比如狗、骨头,狗这个对象又可以包含许多动作,吃东西、吃饱了摇尾巴。
类概念的引入
在C语言中虽然没有类这种概念,但有一个类似的概念—结构体,而C语言中定义结构体的关键字是“struct",C++延续了C语言的传统,保留了"struct"这个关键字,并且在C++中不仅能在结构体内定义变量,还能定义函数。
// 与C语言语法一样,struct是定义结构体的关键字,Student是自定义结构体名
struct Student
{
char _name[20];
char _gender[10];
int _age;
// 在C++中,可以在结构体内定义函数
void SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintStudentInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
};
int main()
{
// 在C++中定义结构体变量,只需要写"自定义结构体名 + 结构体变量名;",不需再要写关键字struct了,当然写上也没有关系
Student s;
return 0;
}
类的概念
事实上啊,以上说的对也不对,我们应该改口了,上面使用关键字"struct"定义的自定义变量不应该在叫结构体了,应该叫类,并且在C++中定义类更喜欢用关键字"class"。
// class是定义类的关键字,在class后面需要写上类名,{}中的就是类的主体
class Student
{
private:
// 类中的所有元素称为类的的成员
// 类中的数据称为类的属性或成员变量
char _name[20];
char _gender[10];
int _age;
public:
// 类中的函数成为类的方法或成员函数
void SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintStudentInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
}s1;
// 与定义结构体一样,}后需加上分号
// 可以像C语言定义结构体变量一样,在}与;中间定义类对象,并且是个全局类对象
int main()
{
// 定义了一个类对象,名字为s,是个局部类变量
Student s2;
// 调用类里面函数的方法是:(类名).(函数名)(实参)
s1.SetStudentInfo("Mike", "男", 18);
s2.SetStudentInfo("Jack", "男", 19);
s1.PrintStudentInfo();
s2.PrintStudentInfo();
return 0;
}
类的定义有两种方式:
第一种:像上面写的代码一样,类的成员的声明和定义都放在类体中,要注意的是:成员函数在类体里面定义的话,在调用时编译器可能会将你这个函数当做内联函数处理。
第二种:类的声明放在一个.h文件中,类的定义放在.cpp文件中。(比较推荐这个方法,不过日常练习图方便可以使用第一种方法)
// person.h
class Person
{
public:
// 在.h文件中只做声明
void SetStudentInfo(const char* name, const char* gender, int age);
void PrintStudentInfo();
private:
char _name[20];
char _gender[10];
int _age;
};
// person.cpp
// 在.cpp文件中就定义函数
// 下面的函数名前的"Person::"先不用管,后面类的作用域会提到这点
void Person::SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Person::PrintStudentInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
类的访问限定符和封装
访问限定符
相信你看上面写的代码也发现了,时不时会出现一些"public"和"private"这两个单词,这就是访问限定符。
访问限定符一共有三个,分别是"public"、“private”、“procteted”,他们可以对类中成员进行访问权限管理。
说明 :
- public意思是公有的,public修饰的成员在类外可以直接访问。
- private表示私有的,protected表示保护,它们修饰的成员在类外不能直接访问。
- 访问权作用域从该访问限定符出现直到下一个访问限定符出现为止
- class定义的类中,如果你不指定访问限定符那么就默认为成员是被private修饰的,而使用struct定义的类中,如果你不指定访问限定符那么就默认为成员是被public修饰的,因为要兼容C语言的struct。
注意:访问限定符只在编译过程中有用,当数据存放到内存后,就没有任何访问限定符上的区别
封装
面向对象有三大特点:封装、继承、多态。接下来我们介绍一下封装,继承和多态以后再进行介绍。
封装实际上就是管理类中成员的访问权限。
举个例子,你去应聘一份工作,需要提供你的相关个人信息,你要把你的名字、年龄、特长等这些个人信息给面试官看到,这样这些信息就是公开的。而你的小金库银行卡卡号只有你知道,并且你也不想让别人知道,你把银行卡藏在床垫底下,这就是私有的,但这就代表你这只银行卡不会被外人使用了吗?并不是,你可能要去ATM机插入银行卡进行一些操作,或者到银行柜台寻找工作人员对你的银行卡进行操作,不过是在一种合理的规则下进行操作。这样就是在管理你的个人信息,对你自己进行一个封装。
类的作用域
在定义类的时候,就相当于定义了一个作用域,类的成员都在这个作用域中。如果你在类中声明了一个函数,不过想在类外定义这个函数,那你就得在函数名前声明这个是哪个类的,并且还要使用作用域解析符::指明这个函数是哪个类中的。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[10];
int _age;
};
// 在类外定义成员,就需要使用::作用域解析符,指明成员属于哪个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
类的实例化
用类类型创建对象的过程,这就是类的实例化。
就像建房子一样,你声明类相当于在画房子的图纸,画完图纸你就有房子了吗?显然不是,还需要照着图纸(类类型)建房子(类对象),并且占用一块地皮(内存空间),这才相当于把图纸里的想法实现了。并且你对着图纸还可以建许多一模一样的房子(建多个类对象),在不一样的位置(不一样的内存空间)有着差不多的房子(有着相同成员类对象),但每间房子的细节不可能都是一样的(实际数据也许不同的类对象)。
类对象
结构体计算大小
上面我们就提到,C++的类和C语言的结构体有点像。那我们先来复习一下计算结构体大小的方法。
计算结构体大小的方法:
- 结构体的每个成员变量都有个对齐数,每个成员变量的对齐数是当前变量大小和默认对齐数(32位操作系统下是4,64位操作系统下是8)中较小的那个,举例:double类型的变量(大小是8个字节)在32位操作系统(默认对齐数是4)下对齐数就是4。
- 第一个成员变量在与结构体偏移量为0的地址处。
- 从第二个成员变量开始,每个成员变量要从当前变量的对齐数的整数倍地址处开始存放。
- 结构体的大小为此结构体中各个变量里面最大对齐数的整数倍,如果结构体里面嵌套了个结构体,那么计算最大对齐数也要把嵌套的结构体中的各个成员变量的对齐数算上。
下面我们来画一下怎么计算结构体大小(建议使用电脑查看):
类计算大小
我们先来了解一下类对象里面的成员是怎么存储的。我们在上面类的实例化中说到,使用类类型可以创建许多不同的对象,且每个对象中的成员变量存储的数值可能是不同的,所以编译器是会拿不同的空间存放每个对象的成员变量的。那如果我们在类类型中定义了函数,然后你使用这个类类型创建了许多对象,需要对每个对象分配空间存储你那功能一样、名字一样、参数列表一样的函数吗?这太浪费空间了,所以编译器会将你在类中定义的函数存储在公共的代码段。
因此我们计算类的大小只需要计算成员变量的大小,并且计算方法与上面的计算结构体大小的方式相同。
那假设我们定义了一个类,这个类中只定义了函数没有定义变量,亦或是定义了一个没有成员的空类,那大小是多少呢?
我们打一段代码,看一下运行结果是什么:
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
// 只定义了函数
class B
{
public:
void f2()
{
}
};
// 空类
class C
{
};
int main()
{
// 因为成员变量只有一个整形变量_a,所以大小为4个字节
cout << "A的大小为:" << sizeof(A) << endl;
// 因为没有成员变量,只有成员函数,理应大小是0才对,但是既然声明了一个对象,如果大小是0的话,
// 又相当于没有定义这个对象
// 所以说编译器给这个类1个字节来唯一标识这个类
cout << "B的大小为:" << sizeof(B) << endl;
// 大小为1的理由与上一个相同
cout << "C的大小为:" << sizeof(C) << endl;
return 0;
}
运行结果:
可以看到,类中只定义函数和空类的大小是1,这是因为尽管这些类中并不存在成员变量,但是这些类依然是存在的,编译器还是给出1个字节声明一下有这些类的存在,
总结:一个类的大小,实际上是类中成员变量大小之和,并且遵循内存对齐规则,而成员函数会存放在公共代码段上,类中只定义函数和空类的大小固定为1,因为编译器会给1个字节标识这些类的存在。
this指针
this指针的引出
我们写一段代码,如下所示:
class Date
{
public:
// 展示日期
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 设置日期时间
void SetDate(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.SetDate(2018, 5, 1);
d2.SetDate(2018, 7, 1);
d1.Display();
d2.Display();
return 0;
}
我们定义了一个Date类,并用Date类创建了两个对象,又分别调用了各自的成员函数,我们走读一下代码,假设代码走到第29行的代码,对象d1调用了Display函数,我们看到我们并没有传任何参数,那编译器怎么会知道是打印d1的成员变量还是d2的成员变量呢?
表面上Display函数参数列表为空,实际上编译器在Display函数的参数列表声明了一个参数,这个参数是"Date const this*",在调用的时候也自动传了对象d1的地址,所以代码实际上是下面这样的,只不过代码将参数列表的指针this和调用函数传的对象的地址,和指针变量名 + ->(即this->)被编译器给隐藏起来让我们看不到而已,并且不需要我们手动传参,编译器会自动帮我们完成。
class Date
{
public:
// 展示日期
void Display(Date* this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
// 设置日期时间
void SetDate(Date * const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.SetDate(&d1, 2018, 5, 1);
d2.SetDate(&d2, 2018, 7, 1);
d1.Display(&d1);
d2.Display(&d2);
return 0;
}
this指针的特性
-
this指针的类型:类型名 const this,因为这个指针固定指向被调用的类对象,所以不可以更改指针指向的地址。*
-
只能在成员函数的内部使用。
-
this指针本质上其实是一个成员函数的形参,是对象调用函数时,将对象的地址作为实参传递给this形参。所以对象不存储this指针。
-
this指针是成员函数的第一个隐含的指针形参,一般编译器会帮我们自动传递被调用对象的地址,所以不需要我们手动传参。(上面代码写上隐含指针形参只是为了演示,实际写代码不需要写)
附加内容
-
this指针是存放在哪里的呢?答案:this指针是存放在栈中的,因为this指针是成员函数中的形参,形参是存放在栈中的。
-
我们看以下这段代码。并提出问题:this指针可以为空吗?main函数中21行、22行代码各自会出现什么问题?能否编译通过?
class A { public: void PrintA() { cout << _a << endl; } void Show() { cout << "Show()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); p->Show(); }
-
第一个问题答案:this指针作为指针当然可以为空,它本质上就是一个普通的指针。
-
第21行代码:可以编译成功,但是PrintA会导致程序运行崩溃。因为虽然可以传空值指针给函数,但是你不能使用,使用相当于nullptr->_a,所以会出现运行崩溃的情况。
-
第22行代码:可以编译成功,并且能成功运行。Show不会出现任何错误,会运行成功,因为你虽然给Show函数传了一个空值指针,但是你没有使用这个指针,只是打印了个字符串,所以不会出现任何错误。
-