目录
1.面向过程和面向对象初步认识
为了解决一个问题,C语言要求我们自己实现各式各样的函数.
比如拿洗衣服举例,可以分为以下几个步骤
也就是说,C语言关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题.
这被称为面向过程编程.
C++则不是如此,它关注的是对象之间的交互.
比如依旧拿洗衣服举例,它可以分为下面几个对象.
我们不需要关注用什么品牌的洗衣粉,洗衣机具体如何工作,甩干衣服等等,只需要关注每个对象
之间的关系是什么,人往洗衣机加衣服,放洗衣粉,启动洗衣机,就可以完成洗衣的过程.
这被称为面向对象编程.
2.类的引入
而从C的面向过程迈向C++面向对象编程,我们必须先了解类的概念.
C语言的结构体我们已经比较熟悉了,比如说下面栈的结构体.
typedef int STDataType;
struct Stack {
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
C++的结构体发生了变化,功能上更加强大,可以在结构体里面放入相应的函数.
于是我们的栈结构体,就可以升级为下面的这种形式.
typedef int STDataType;
struct Stack {
void StackInit(int InitSize = 4)
{
STDataType* newdata = (STDataType*)malloc(sizeof(int) * InitSize);
if (newdata == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
a = newdata;
capacity = InitSize;
top = 0;
}
...//可以放更多有关栈的函数
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
和C语言不同,C++更习惯于用Class来替代struct结构体(两者的区别我们后面会提及)于是上面的
代码又可以变成下面这种形式.
typedef int STDataType;
class Stack {
void Init(int InitSize = 4)
{
STDataType* newdata = (STDataType*)malloc(sizeof(int) * InitSize);
if (newdata == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
a = newdata;
capacity = InitSize;
top = 0;
}
...//可以实现更多和栈相关的函数
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
总结:
1.可加函数,而且函数名不需要再命名为StackInit,直接Init即可,毕竟不同类调用的初始化函数是不同的.
2.不再需要typedef重命名,像Stack直接就是类名.
3.类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分
号不能省略.
类体中有两大内容
一个是类的成员:类中的变量称为类的属性或成员变量;(比如上面的top,capacity)
另一个是类中的函数称为类的方法或者成员函数(比如上面的StackInit函数)
1.成员变量
我们先看下面这段Date类的初始化,由于成员变量中含有year变量,函数形参也是year的命名,则
初始化显得会非常僵硬.
class Date
{
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
int year;
};
一般我们对于类中的成员变量,命名前会加_,以作区分.
class Date
{
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
_year = year;
}
int _year;
};
2.成员函数
和之前函数实现一样,类中的成员函数有两种定义方式.
第一种是声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成
内联函数处理.
typedef int STDataType;
class Stack {
void Init(int InitSize = 4)
{
STDataType* newdata = (STDataType*)malloc(sizeof(int) * InitSize);
if (newdata == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
a = newdata;
capacity = InitSize;
top = 0;
}
void Push(int x)
{
//如果栈满,及时扩容
if (capacity == top)
{
STDataType* tmp = (STDataType*)realloc(a, capacity * 2 * sizeof(STDataType));
if (tmp == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
else
{
a = tmp;
capacity *= 2;
}
}
a[top] = x;
top++;
}
...//有挂Stack的函数
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
第二种是类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
也就是在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域
//Stack.h
#include <stdio.h>
#include <stdlib.h>
typedef int STDataType;
class Stack {
void Init(int InitSize = 4);
void Push(int x);
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
//Stack.cpp
#include "Stack.h"
void Stack::Init(int InitSize)
{
STDataType* newdata = (STDataType*)malloc(sizeof(int) * InitSize);
if (newdata == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
a = newdata;
capacity = InitSize;
top = 0;
}
void Stack::Push(int x)
{
//如果栈满,及时扩容
if (capacity == top)
{
STDataType* tmp = (STDataType*)realloc(a, capacity * 2 * sizeof(STDataType));
if (tmp == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
else
{
a = tmp;
capacity *= 2;
}
}
a[top] = x;
top++;
}
PS:缺省参数,一般放在声明,定义和声明不需要同时出现缺省参数.
一般情况下,更期望采用第二种方式。注意:工作中尽量使用第二种.
4.类的访问限定符及封装
1.访问限定符
我们之前在实现栈的时候曾经讨论过一个问题,就是top初始是0还是-1的问题.
如果top == 0,则top指向的始终是栈顶元素的后一个元素,而top == -1,情况又会有所不同.
假如有人实现了一个Stack类,我们调用它进行完成任务.
他自然不希望有人对它的Stack类中的top成员变量进行修改.
也就是类中的成员变量,我们希望它得到保护,而别人只能调用类中的方法.
于是我们引入了访问限定符的概念.
于是上面的程序,我们又可以修改为下面的形式.
typedef int STDataType;
class Stack {
public:
void Init(int InitSize = 4);
void Push(int x);
private:
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
不同访问限定符之间的区别在哪呢?
一.public(公有)修饰的成员在类外可以直接被访问
二.protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)PS:
1.访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
2.访问限定符有它的作用范围
作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
如果后面没有访问限定符,作用域就到 } 即类结束
了解访问限定符后,我们可以给出struct和class两者之间的区别之一.
struct定义的类默认访问权限是public,class定义的类默认访问权限是private.
2.封装
面向对象有三大特性:封装,继承,多态.
在本节,我们主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互.
举一个形象的例子,一部电脑是由各式各样的组件构成的,有CPU,显卡等等.这其实就相当于一个
个类,我们不需要关心它是如何组成的,计算机厂商在出厂时,在外部套上壳子,将内部实现细节
隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,用户就可以与计算机进行交互.
同样的道理,还是拿Stack类举例,我们不需要关注别人是如何初始化它,只需要直接调用Stack类
里面的初始化函数,我们就可以对对象进行相应的初始化.
封装本质上是一种管理,让用户更方便使用类.
5.类的实例化
用类类型创建对象的过程,我们称之为类的实例化.
比如说像之前我们struct例子,我们是并没有创建一个对象的.
我们只是自定义了一个叫做Stack的类,它仅仅相当于声明的作用.
定义出一个类并没有分配实际的内存空间来存储它
typedef int STDataType;
class Stack {
public:
void Init(int InitSize = 4);
void Push(int x);
private:
STDataType* a;
int top; //栈顶下标
int capacity; //栈的容量
};
我们在main函数,或者其它地方,实际创建出一个叫做st1的对象,就是我们说的类的实例化.
int main()
{
Stack st1;
return 0;
}
类就像一张张建筑图纸或者说模板,我们可以根据它实例化出多个对象,实例化出的对象才占用实
际的物理空间,存储类成员变量.
6.类的对象大小的计算
我们知道每个对象成员变量是不一样的,需要独立存储.
相反,每个成员调用的成员函数都是一样的,我们可以直接放到共享公共区域,也就是代码段.
因此,如果计算类的大小
编译器只会按照计算结构体的方式计算类中所有成员变量的大小.
具体规则有以下四点:
1)结构体的第一个成员直接对齐到相对于结构体变量起始地址为0的地方
2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
3)如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
4)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍.
PS:注意空类,也占据一个字节的大小.
目的是占位,标识对象被实例化定义出来了
7.类成员函数的this指针
了解上面的内容后,我们可以简单构造出名为Date的类.
#include <iostream>
using namespace std;
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(2023, 2, 3);
d2.Init(2003, 3, 20);
d1.Print();
d2.Print();
return 0;
}
我们根据Date类,实例化出了两个对象,一个d1,另一个d2,并分别对它们进行初始化和打印数据.
不过成员函数都放在类里面,编译器是如何知道初始化哪个对象,或者打印哪个对象呢?
其实和之前一样,每个对象都有它自己的地址,在调用相应的非静态成员函数时,编译器会默认将
对象的地址,传给this指针(形参)
PS:
1.不能说this指针存在于对象之中,比如说有一个函数,没有访问成员变量,单纯只是输出一堆无关字符,则不需要用到this指针.
2.this指针虽然不能赋值为空,但可以强转直接赋空,不过一般不进行这样的操作
只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成.
所以实际上的代码是这样子的:
class Date
{
public:
void Init(Date* this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
不过由于代码这样书写,反而没有起到简化程序的作用,通常this指针,我们都默认不写.
我们知道this指针是一个临时隐含形参,所以和所有形参一样,它是在栈上开辟的.
当然不同编译器又有所不同,比如VS2019编译器下面是通过ecx寄存器进行存储的.
同时,我们也可以知道,既然this是一个指针,所以它也能为空,不过此时就一定不能对它进行解
引用,否则程序就会发生奔溃.
比如说下面这段程序,程序会崩溃,并提示用户ptr为空指针.
class Date
{
public:
// 定义
void Init(Date* this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void func()
{
cout << "func()" << endl;
}
private:
int _year; // 声明
int _month;
int _day;
};
int main()
{
Date* ptr = nullptr;
ptr->Init(2022, 2, 2); // 运行崩溃
return 0;
}
但假如本身类中的函数,是不需要用到this指针的,比如直接打印一堆字符串等等,像下面的
func函数,程序是能够正常运行的.
class Date
{
public:
// 定义
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void func()
{
cout << "func()" << endl;
}
//private:
int _year; // 声明
int _month;
int _day;
};
int main()
{
Date* ptr = nullptr;
ptr->func(); // 正常运行
(*ptr).func(); // 正常运行
return 0;
}
八.C++和C的对比
两者就好像汽车中的自动挡和手动挡区别,Cpp就是自动挡,C就是手动档.
总结:
Cpp:
1.数据和方法都封装在类里
2.控制访问方式.愿意给你访问的公有,不愿意给你访问的私有.
C:
1.数据和方法分离2.数据访问和控制是自由的,不受限制,但同时也说明C的不安全性,需要了解底层结构.