1.面向对象和面向过程的初步认识
面向过程和面向对象是一个什么东东?我们得把这个问题弄清楚。
面向过程:是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。
简而言之:C语言就是面向过程的语言,我们把要完成的内容,一步一步交给函数完成。
如之前写的三子棋:初始化棋盘,打印棋盘,玩家下棋,判断输赢,电脑下棋,判断输赢。每一步都是一个个函数,把整个程序都交给一个个函数,这就是面向过程。
**面向对象:**我们关注的是对象,把一件事拆分成不同的对象,靠对象之间的交互完成。
三子棋就可以分为:玩家,电脑对象,棋盘对象,判断输赢系统对象。把具体的步骤交给每个对象,而三子棋只用考虑对象之间的交互即可。
而C++就是面向对象的语言,但是C++不是纯面向对象的语言,它是基于面向对象的语言,它是既可以面向对象又可以面向过程的语言,因为C++兼容C语言,像隔壁的兄弟JAVA就是纯面向对象的语言,写一个普通的交换函数,都需要用类才能完成。
2.类的引入
在C语言的结构体中,只能定义变量,但是在C++的结构体中,可以定义变量,也可以定义函数。之前的结构体我们是把函数放在结构体外面实现的,但是现在C++中我们就可以把函数和变量一起写到结构体里面。
typedef int DataType;
struct Stack
{
void Init(size_t capacity)//函数
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const DataType& data)
{
// 扩容b
_array[_size] = data;
++_size;
}
DataType* _array;//变量
size_t _capacity;
size_t _size;
};
但是在C++中我们习惯把struct换成class来定义,后面会说到。
3.类的定义
类的定义和结构体的定义是大差不大的,把struct换成class即可,class里面多了可以定义函数,还有范围权限(后续说)
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
-
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
//第一种:声明和定义全部放在类体中 class people { void showinfo() { cout << name << "-" << sex << "-" << age << endl; } char* name; char* sex; int age; };
-
类声明放在.h文件中,成员函数定义放在.cpp文件中。
对于栈来说,我们实例化结构体时,需要struct stack st,但是在类中我们只需要stack st,即可创建出对象,显而类是更加方便的。
注意:成员函数名前需要加类名::
工程项目中几乎都是使用第二种形式,但是我们学习为了方便,大多都写成第一种形式。
成员变量命名规则的建议:
// 我们看看这个函数,是不是很僵硬?
class Date
{
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
int year;
};
如果像上述这样写,我们就没法分清楚year到底是什么?
所以我们对于成员变量命名上一般都加一杠,如下所示:
class Date
{
void Init(int year)
{
_year = year;
}
int _year;
};
4.类的访问限定符及封装
4.1 访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符说明:
①public修饰的成员在类外可以直接被访问 。
②protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
③class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
这里实例化出类的对象,但是访问不了Init函数,这是因为类的默认权限是private,类外是不能访问的,如果要访问,就只能加上public访问限定符。
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
int main()
{
Date d1;
d1.Init(10);
return 0;
}
4.2 封装
C++的三大特性:封装,继承,多态。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
总之:封装是一种合理,严格的管控!
5.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
6.类的实例化
对类类型创建对象的过程就是类的实例化。
当类还没有创建出对象的时候,它是像一个模型一样,并没有分配实际的内存空间。
类就像是建房子的图纸一样,而实例化出来的对象就像是使用图纸建好的房子一样。
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
int main()
{
Date d1;//这样就是类实例化出对象
return 0;
}
7.类对象模型
7.1 如何计算类对象的大小
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
比如下面这个代码,类对象的大小是多少呢?
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
int* _b;
};
7.2 类对象的存储方式
这里可以看出类对象的大小都是8,这里无论你sizeof类还是sizeof实例化出来的对象,其实都是一样的。其实这个8只是对象中成员变量的大小,只包括char和int*,这里使用了内存对齐规则,和C语言的规则是一样的,以前的文章有说:自定义类型之结构体,枚举和联合。
如果把类成员变量和类成员函数都存到内存的话。
**缺陷:**每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
其实只保存成员变量,成员函数存放在公共的代码段。
这里还有一个注意的地方,空类的大小是多少呢?
class A
{
};
int main()
{
cout << sizeof(A) << endl;
}
是不是感觉好奇怪,空类既没有成员变量也没有成员函数,它的大小应该是0才对啊,但是你有没有想过,如果空类的大小是0,也就是内存中没有空类的地址,那如何找到该空类呢?所以为了解决这种麻烦,内存会为空类开辟一个字节。
8.this指针
8.1 this指针的引出
为了好的学习this指针,我们来定义一个日期类Date。
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 main()
{
Date d1;
Date d2;
d1.Init(2022, 1, 11);
d2.Init(2024, 1, 12);
d1.Print();
d2.Print();
return 0;
}
可以看出两个对象都分别打印出来了自己的日期。但是有个问题?
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函 数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
这里确实是d1和d2的Init函数的地址都是一样的。
这时C++就引入一个this指针来解决这个麻烦。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
像上述的代码编译器就会写成这样:
还有这个成员函数print看似没有参数其实有个隐含的this指针。
void Print(Date*this)
{
cout << this._year << "-" << this._month << "-" << this._day << endl;
}
这个this指针是编译器加上去的,我们不能显示的写出来。
8.2 this指针的特性
-
this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
void Print(Date*this) //其实是void Print(Date* const this) //const在*的后面,修饰的是this指针本身,即不能修改this指针
-
只能在“成员函数”的内部使用 。
-
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
-
this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
这里有两个题试试吧!
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}