目录
一、面向过程和面向对象初步认识
1.C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
2. C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完
成。
以洗衣服为例子
c语言关注
c++关注
二、类的引入
鉴于C++面向对象的特点,于是就创建出类这个概念来处理每个对象,我们可以通过类来描述每个对象的全部特点
但是虽然c++是基于面向对象的语言,关注对象,但是它也支持面向过程,因此c语言和c++的代码可以混编
c++兼容c语言,结构用法可以继续使用
同时struct也升级成了类
struct Stack//类名
{
int* a;
int top;
int capacity;
};
int main()
{
struct Stack st1;//结构体的用法
Stack st2;//类的用法,类名可作类型
};
同时,类中不止可以定义变量,也可以声明和定义函数
不用我们主动传参,成员函数可以直接访问成员变量,例子如下
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;
}
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s;
s.Init(10);
return 0;
}
命名规则
为了避免函数形参与成员变量之间产生混淆等 ,我们一般将成员变量加上标记处理,比如_,my等等。如果形参与成员变量名称相同,默认优先使用局部域的变量,就会形成局部变量自己赋值给自己的情况
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
// 或者这样
class Date
{
public:
void Init(int year)
{
mYear = year;
}
private:
int mYear;
};
三、类的定义
但是c++中更喜欢使用class,主要原因在4.访问限定符中讲
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者
成员函数。
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器会将其当成内
联函数处理。
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
,因为编译器遇到一个变量要去找,加了类名它才知道去哪找,不加类名它只会默认在全局去找
一般情况下,更期望采用第二种方式
四、访问限定符
C++实现封装的方式:
用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
【访问限定符说明】
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } ,即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C),因此我们在c++中更喜欢使用class
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
一般来说我们习惯将成员变量私有,通过公有的成员函数进行访问,同时,类里面不受访问限定符的限制。
五、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
我们可以将花括号就看成一个域的界限
六、类的实例化
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 d;//这里开了空间,这才叫定义
return 0;
}
那三个成员变量都绑在一起定义了, 对象的定义也叫做对象的实例化。
同样的,函数的定义和声明也是一样,定义需要分配存储空间;声明只是告诉编译器这个函数已经在别处定义过了。
函数的定义和声明比较好区分。有函数体的即为定义,不带函数体即为声明。
七.类对象模型
接下来再看另一个问题
int main()
{
date d1;
d1.init(2024,7,12);
d1.Print();
d1._year;
date d2;
d1.init(2024,7,13);
d1.Print();
d2._year;
}
我们sizeof(date) 与sizeof(d1)得出的值均为12(实际上,sizeof计算对象的大小也是转换成对对象类型的计算)
我们知道,d1中的_year 与d2中的_year不是一个变量,但是d1中的Print()与d2中的Print()是同一个函数,如果我们在每个对象中都存一个,这种方式不划算。所以d1.year 与d2._year是到对象里面去找,但是d1.Print()与d2.Print()是到公共的代码区去找。成员函数与全局函数都储存在公共代码区
因此一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。即占位
// 类中什么都没有---空类
class A
{};
知识补充
结构体内存对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
那我们为什么要内存对齐呢?
假设我们的cpu从起始位置,每次4字节地访问内存(这个和具体的cpu有关系)
我们的假设我们内存没有对齐,cpu并不能从_ch后面一个字节开始访问,因此访问_a需要访问两次,而且还涉及到数据拼接的过程
八、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 main()
{
Date d1, d2;
d1.Init(2024,7,11);
d2.Init(2024,7,12);
d1.Print();
d2.Print();
return 0;
}
Init函数看似传了三个参数实际上传了四个参数,Print看似没有传参数实际上传了一个参数,这个隐含的参数就叫this指针
实际编译器处理的是如下形式,但是我们不能自己添加this指针
void Init(date* const this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
d1.Init(&d1,2024,7,11);
d2.Init(&d2,2024,7,12);
但是值得注意的是,this 虽然不能在形参和实参位置显式写,但是在类里面可以显式用。如下
void Init(int year, int month, int day)
{
cout << this << endl;
this->_year = year;//但是我们一般不这么用,因为编译器自动会给我们处理
this->_month = month;
this->_day = day;
}
成员函数之间也可以互相调用
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
Print();//实际上为this->Print();
}
先在公共代码区根据名字找到Print()的指针,然后把this指针传进去。
下一个问题,显示代码如下
p->Print(),解引用了吗?
这个函数没有存在对象里面,所以没有进行解引用。
全部过程是,先把p传到ecx寄存器中,对于函数则是在公共的代码区域拿着这个名字去找它的地址,即call一个地址,编译阶段就填上了。
然后再把p传递给这个this指针。
下面这种情况才是解引用
因为_a变量是存在对象里的
我们再看一种类似的情况
这种情况会运行崩溃。
虽然在main函数中的过程与上述相同,并没有解引用。
但是在Print()函数内部,传入this指针后进行了this->_a的操作,进行了解引用 。