目录
1、前言
2、类的引入
3、类的定义
3.1 类的两种定义方式
4、类的访问限定符
5、类的作用域
6、类的实例化
7、类对象模型
7.1 内存对齐规则
7.1 类对象的存储方式
8、this指针
8.1 this指针的特性
8.2 this指针是否可以为空
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
我们来举列子说明一下:
比如蒸米饭这件事,C语言关注的就是,淘米 -> 加水 -> 加米 -> 起锅 -> 烧锅 -> 水沸腾 -> 放入盛有生米的盆 -> 烧锅 -> 拿出;C++在这里关注的是对象:人、米、水、电饭煲,人不需要知道电饭煲如果工作的,只需要四个对象之间交互完成任务就可以。
C语言结构体(struct)中只能定义变量。
而在C++中,新增加了玩法,结构体内不仅可以定义变量,也可以定义函数。
举例:
struct Person
{
void personInit()
{
cout << "void personInit()" << endl;
}
int _age;
char _name[20];
};
int main()
{
Person p;
p.personInit();
return 0;
}
上面结构体的定义,在C++中更喜欢用class来代替。
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
3.1.1 声明和定义全部放在类体中
需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Person
{
public:
void PersonInit()
{
cout << "void PersonInit()" << endl;
}
private:
int _age;
char _name[20];
};
3.1.2 类声明放在.h文件中,成员函数定义放在.cpp文件中
注意:成员函数名前需要加类名::
//Person.h
class Person
{
public:
void PersonInit();
private:
int _age;
char _name[20];
};
//Person.c
#include "Person.h"
void Person::PersonInit()
{
cout << "void Person::PersonInit()" << endl;
}
一般情况下,更期望采用第二种方式。
注意:一般在声明成员变量的时候成员变量前加_(下划线),为区分成员函数的形参(只要能区分就可以,前家下划线是我的一种方式,谷歌C++规范一般喜欢后加_)。
如果成员变量没有特殊标记时,当在成员函数内用到成员变量,并为其赋值的时候,函数会采用局部优先的原理,将自己赋给自己,这样就达不到预期的效果。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选
择性的将其接口提供给外部的用户使用。
【访问限定符说明】
1. public修饰的成员在类外可以直接被访问;
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的);
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
4. 如果后面没有访问限定符,作用域就到 } 即类结束;
5. class的默认访问权限为private,struct为public(因为struct要兼容C)。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
例如:
class Person
{
public:
void PersonInit();//声明
private:
int _age;
char _name[20];
};
//初始化定义的时候函数名前加 Person::,表明PersonInit是Person类域的
void Person::PersonInit()//定义
{
cout << "void Person::PersonInit()" << endl;
}
用类类型创建对象的过程,称为类的实例化。
在实例化之前,定义出来的类是不占空间的。
例如:
class Person
{
public:
void PersonInit()
{
cout << "void PersonInit()" << endl;
}
private:
int _age; //声明
char _name[20];
};
int main()
{
Person::_age = 20;
//定义开空间
Person p;
return 0;
}
在没有实例化p的时候,定义的 class Person 类不会开辟空间。
实例化出了 p 对象,这时 p 是占用实际的空间的,存储了成员变量。(这里实例化后,p也不能直接用_age,因为是私有的,后面会讲如何赋值,打印)。
类就像是设计图。
类实例化出对象就像现实中使用设计图盖房子。
在没有盖房子之前,这块区域没有占空间。
实例化就是按图盖房子,这时就占用了空间。
我们可以使用设计图盖多个房子,这些房子都占空间
如何计算类对象的大小呢?
例如:
class A
{
public:
void AInit(int a, int b)
{
cout << "void AInit(int a, int b)" << endl;
}
private:
int _a;
int _b;
};
class B
{
private:
int _a;
int _b;
};
class C
{};
int main()
{
cout << "类A的大小:" << sizeof(A) << endl;
cout << "类B的大小:" << sizeof(B) << endl;
cout << "类C的大小:" << sizeof(C) << endl;
return 0;
}
运行结果:
总结:
1、成员函数不算在类的大小中;
2、类的大小只与成员变量有关,并遵循结构体对齐规则;
3、空类的大小为1字节(不存储数据,只是占位,表示对象存在过)。
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么类中的成员函数不占空间?那成员函数是存在哪里呢?
实例化后的类中只存储类成员变量
成员函数保存在公共的代码段。
我们画图来理解一下:
我们这里写一个日期类来看一下:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2023, 7, 30);
return 0;
}
这里看似 Init 函数只有三个形参
调用的时候传了三个参数
实际上,这里还隐含了一个 this 指针。
我们这里画图来看一下:
这里对成员变量赋值的时候,前后都会加一个 this-> 来接引用访问。
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用。
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
因此,我们写的时候就不可以这样写:
class Date
{
public:
void Init(Date* this, int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(&d1, 2023, 7, 30);
return 0;
}
this指针隐含着,如果我们自己加上就是错误的。
this在实参和形参的位置上不能显示写
但是在类里面可以显示的用
如下:
class Date
{
public:
//this在实参和形参的位置上不能显示写
//但是在类里面可以显示的用
void Init(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;
d1.Init(2023, 7, 30);
return 0;
}
我们来看下面几段代码:
class Person
{
public:
void PersonInit()
{
cout << "void PersonInit()" << endl;
}
private:
int _age; //声明
char _name[20];
};
int main()
{
Person* p = nullptr; //初始化为空指针
p->PersonInit();
return 0;
}
运行结果:
这里我们就会产生疑问,p是空指针为什么可以解引用呢?还是正常运行。
这里对于函数定义在类里面且短小,编译器会当作内联函数,直接展开,并不会解引用;
而如果声明与定义分离或者编译器不将其当作内联函数,就是call Init函数(调用函数)的地址,也不是解引用。
我们继续看:
class Person
{
public:
void PersonInit()
{
cout << "void PersonInit()" << endl;
}
//private:
int _age; //声明
char _name[20];
};
int main()
{
Person* p = nullptr; //初始化为空指针
p->PersonInit();
p->_age = 1;
return 0;
}
这就会导致运行崩溃,对空指针的内容进行解引用。
我们接着上面看:
class Person
{
public:
void PersonInit()
{
cout << _age << endl;
}
//private:
int _age; //声明
char _name[20];
};
int main()
{
Person* p = nullptr; //初始化为空指针
p->PersonInit();
return 0;
}
这里在调用Init函数的时候,函数里面产生了解引用,但是this是空指针,这里就会运行崩溃。
空指针不会编译错误,只会导致运行崩溃。