目录
一、面向对象是什么?
我们都说C++是面向对象的程序设计语言,但是什么是面向对象呢?
面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的编程典范,同时也是一种程序开发的抽象方针。它可能包含数据、特性、代码与方法。对象则指的是类(class)的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象[1][2]。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。反对者在某些领域对此予以否认。
当我们提到面向对象的时候,它不仅指一种程序设计方法。它更多意义上是一种程序开发方式。在这一方面,我们必须了解更多关于面向对象系统分析和面向对象设计(Object Oriented Design,简称OOD)方面的知识。许多流行的编程语言是面向对象的,它们的风格就是会透由对象来创出实例。
举一个浅显的例子:如果要设计一个外卖平台,C语言可能会更加关注函数,例如如何点餐,如何送达,平台的界面等等 而C++会关注 外卖员,商家,顾客 这三个对象
C语言是面向过程,分析求解问题的步骤,通过函数调用逐步解决问题
C++是基于面向对象的,关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成
二、类的引入
1、struct
C++中也有struct,在C语言中struct是结构体的关键字,用来定义一个结构体,结构体中只能有变量不能有函数,并且结构体中的变量在外部可以被访问:而C++中的struct被升级了,它升级成为了一个类,它的内部有成员变量(属性)和成员函数(行为),并且它兼容了C中的结构体的全部用法
#include<iostream>
using namespace std;
struct Person
{
void showPerson()
{
cout << _name << endl;
cout << _age << endl;
cout << _tel << endl;
}
char _name[20];
int _age;
int _tel[20];
};
int main()
{
Person p1;
p1.showPerson();
return 0;
}
我们一般定义一个类所用的关键字是class
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
2、类的两种定义方式
1、声明和定义全部放在类中,在符合内联的条件 编译器可能会将其当作内联函数来处理
内联的条件:函数较短,调用频繁,没有递归
class Person
{
public:
void showPerson()
{
cout << _name << endl << _age << endl << _tel << endl;
}
private:
char _name[20];
int _age;
int _tel[20];
};
这是将定义和声明全部放在类中
2、声明放在类的头文件中,定义放在类的实现文件中
//Person.h
class Person
{
public:
void showPerson();
private:
char _name[20];
int _age;
int _tel[20];
};
#include"Person.h"
//Person.cpp
void Person::showPerson()
{
cout << _name << endl << _age << endl << _tel << endl;
}
综上所述,类的定义应该满足这样的原则
1.小函数,想成为inline,直接在类中定义
2.大函数,声明和定义分离
我们已经将类的框架定义好了,这时有一个疑问, 类定义的成员变量是定义?还是声明?
我们要明确一点:对于函数来说很容易就可以区分定义和声明,如果有函数体就是定义,如果没有函数体就是声明,而对于变量来说,就不是很好来区分了
声明提供了变量的类型和名称但是没有开辟空间。
定义:开辟空间的,且只能定义一次
声明:告知,可以声明多次
这也就从侧面反映出类中的是声明而不是定义
3、访问限定符
数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。
一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。
1.public修饰的成员在类内外均可访问
2.private和protected修饰的成员只能在类内访问
3.访问限定符从该限定符开始直到下一个限定符结束,如果没有其它限定符,那么就到类的结束
4.struct默认访问权限是public,class的默认访问权限是private
4、类的作用域
接着以上面的例子为例:我们类的第二种定义方式中利用了::作用域解析符号来指明show函数位于哪个作用域,如果去掉就会报错
C++中花括号括起来的就是一个域,域要么是用来限定访问空间,要么是影响变量的声明周期
还要补充两个小知识点
局部变量:包含在代码块中的变量叫做局部变量,局部变量具有临时性,进入代码块,自动形成局部变量,退出代码块自动释放
全局变量: 在所有函数外定义的变量,叫做全局变量,全局变量具有全局性
代码块:用{ }括起来的区域
变量的生命周期
生命周期指的是该变量从定义到被释放的时间范围,所谓的释放,指的是曾经开辟的空间被释放
也就是说作用域是该变量的有效区域
声明周期是一个时间概念,什么时候被开辟,什么时候被释放
有了这些补充就可以充分理解类的作用域了。
为什么要有类,为什么要限制访问?
项目维护提供安全保证下不会暴露过多的代码细节。
C语言是没有办法封装的,我们可以访问函数也可以直接访问数据,要求程序员的素养高,并且这种用法也不规范
C++可以进行封装,能够规范的使用函数来访问数据,不能够直接访问数据,安全性高,用法规范。
5、类的实例化
用类类型创建对象的过程,叫做类的实例化
实例化是指在面向对象的编程中,把用类创建对象的过程称为实例化。是将一个抽象的概念类,具体到该类实物的过程。实例化过程中一般由类名 对象名 = new 类名(参数1,参数2...参数n)构成。
类实例化对象,就相当于使用图纸盖房子,类就是图纸,同时类中不含成员函数,如果包含就会出现大量的空间浪费,成员函数没有存到对象中,函数储存在公共代码段,计算类的大小只计算成员函数的大小
这种对象存储方式决定了,实例化的每个对象成员都是独立空间,是不同的变量,但是每个对象调用的成员函数都是同一个
我们定义两个Person类然后观察汇编代码,发现它们的show函数的地址是相同的
这也就侧面说明了前面的结论是正确的。
6、类的大小
一个类的大小,实际就是该类中成员变量之和,要注意内存对齐
内存对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8,Linux没有默认对齐数。3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们有一个疑惑,空类的大小是多少呢?
我们定义了一个空类P,然后计算它的大小
class P{};
int main()
{
cout << sizeof(P);
return 0;
}
结论:空类的大小不是0是1,给1个字节不是为了存储数据,而是为了表示对象的存在
7、this指针
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象
我们已经了解了同一个类实例化出的不同对象所调用的是同一个成员函数,而调用的方法就是通过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 a;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
我们定义了一个日期类,实例化出d1和d2两个对象调用的是同一个Init函数和Print函数
它的调用实际上是这样的
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 a;
};
int main()
{
Date d1, d2;
d1.Init(&d1, 2022, 1, 11);
d2.Init(&d2, 2022, 1, 12);
d1.Print(&d1);
d2.Print(&d2);
return 0;
}
哪个对象调用函数,this指针就指向哪里,不过我们不能显示的传入this指针,它是在编译时编译器自动加入的,我们可以在成员函数中使用
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
thsi->_month = month;
this->_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
int a;
};
this指针一般是存在栈里面,有一些编译器是存在寄存器里面
既然我们已经了解了this指针,那么看以下几个问题
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
答案是第一个是正常运行的,虽然我们的对象是nullptr this指针也的确将nullptr传到print函数中,但是没有解引用访问对象中的成员,传入空指针没有问题,解引用之后才会出现段错误
第二个解引用去访问了_a所以会出现段错误,运行崩溃。