文章目录
-
- 面向对象和面向过程的区别
- 类的定义
- 类的访问限定符和封装
- 类的作用域
- 类的实例化
- 类对象大小的计算
- [this 指针](#this 指针)
二次修订于date:2024:3:6
面向对象和面向过程的区别
C语言是一们面向过程的语言,关注的是函数执行的过程,数据和方法是分离的。
C++是一门面向对象的语言,主要关注对象,将一件事情抽象成不同的对象,靠对象间的交互来完成。对象与对象之间的交互叫做消息(举例外卖系统中,分为快递小哥,商家,菜品,用户这几个类,通过这些类创建出各自的对象,通过他们的交互来完成一次外卖行为)
类的定义
关于类的定义这里因为C++是兼容C语言的,所以C++给C语言的struct一个新的定义,就是用来定义类,C语言中关于结构体的用法C++这里同样也是兼容的。
cpp
struct name
{
void Init()
{
;
}
void Push()
{
;
}
int* _a;
int _top;
int _capacity;
};
定义一个类实际就是一个类型(花括号括起来的就是一个作用域,类的作用域就叫类域),类里面可以定义成员变量_top等,也可以定义成员函数就是Init和Push。name就是类名,成员变量和成员函数组成了类体。
struct毕竟是C语言的产物,C++还有一个关键字class来定义类
cpp
class stu
{
public:
void Init(const char*arr,const char*id,const char*sex)
{
strcpy(_name, arr);
strcpy(_id, id);
strcpy(_sex, sex);
}
char _name[20];
char _id[15];
char _sex[5];
};
int main()
{
struct stu s;
s.Init("zhangsan", "0007", "nan");
return 0;
}
类里面定义实现的函数会被编译器默认当成内联函数。
声明与定义分离如下所示
cpp
//main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"stu.h"
int main()
{
stu s;
s.Init("zhangsan", "0007", "nan");
return 0;
}
//stu.h
#pragma once
#include<iostream>
#include<cstring>
using namespace std;
class stu
{
public:
void Init(const char* arr, const char* id, const char* sex);
char _name[30];
char _id[20];
char _sex[10];
};
//stu.c
#define _CRT_SECURE_NO_WARNINGS
#include"main.h"
void stu::Init(const char* arr, const char* id, const char* sex)
{
strcpy(_name, arr);
strcpy(_id, id);
strcpy(_sex, sex);
}
类的访问限定符和封装
上面代码中的public就是类的访问限定符。
类的访问限定符一共有三个:public,private,protected在此处暂且认为private和protected是相同的。其中public的意思是公有,在public的范围之下(从public出现的地方到下一个访问限定符出现或者到类结束},这段范围都是public)的成员函数或者成员变量是可以在类外被直接访问到的。比如上面代码中的成员函数Init或者成员变量id_[20],可以直接在外面被修改。但是在private或者protected的范围内的成员在类外是不可见的。
cpp
//main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"stu.h"
int main()
{
class stu s;
s.Init("zhangsan", "0007", "nan");
return 0;
}
//stu.h
#pragma once
#include<iostream>
#include<cstring>
using namespace std;
class stu
{
public:
void Init(const char* arr, const char* id, const char* sex);
private:
char _name[30];
char _id[20];
char _sex[10];
};
//stu.c
#define _CRT_SECURE_NO_WARNINGS
#include"main.h"
void stu::Init(const char* arr, const char* id, const char* sex)
{
strcpy(_name, arr);
strcpy(_id, id);
strcpy(_sex, sex);
}
在stu类里面加了一个private,这样就限定类外不能直接访问到成员对象,只能通过成员函数来访问成员对象,好处就是,用户的行为都在掌控之中,不会出现,直接修改成员对象导致某些错误的情况发生。
如果不加类访问限定符,那么struct默认为public(因为struct要兼容C语言的结构体所以是能够随意访问的),class默认为private。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别(后面的多态,会存在方法突破类域限制)
**【面试题】 **
**问题:C++中struct和class的区别是什么? **
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。 和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。
封装
封装实际上是将数据与操作数据的方法结合起来,然后隐藏对象的部分属性和实现细节(一般成员变量都是隐藏的)对外只公开接口,通过接口来对数据进行操作。
封装可以使得数据更加安全,不存在说用户直接去修改对象属性的这种危险行为,用户只能通过调用函数接口来访问对象,行为更加可控。
所以想给外部展示的成员定义为public,不想给外部展示的定义为private或protected。
类的作用域
定义一个类,这个类的大括号就是这个类的作用域,又叫做类域。想要类外实现类成员,就要指定函数属于那个域。
cpp
class stack
{
public:
void Init();
int size;
int capacity;
int* a;
};
void stack::Init()
{
//.....
}
在类外定义类成员就要加stack::。
c
//stack.cpp
void stack::Init()
{
.....
}
类的实例化
用定义好的类创建出来对象,这个就叫做类的实例化。
cpp
class stack
{
public:
void Init()
{
;
}
void Push()
{
;
}
private:
int*_a;
int _size;
int _capacity;
};
int main()
{
stack s1;
s1.Init();
s1.Push();
return 0;
}
用stack创建出来了一个对象s1,这个就是类的实例化。
仅仅定义一个类是不占用内存的,是创建了一个类型,这相当于是一个蓝图,通过这个蓝图可以创建对象,当对象创建出来之后才会在内存中占用空间。
stack::_a为什么不能访问到a?因为stack类型里实际并没有变量a,要等实例化对象之后才存在_a变量。
static类型的成员变量是一个例外。
类对象大小的计算
这里要注意当计算s1的大小的时候,计算出来的大小是12,那么问题来了,成员函数不占内存吗?
因为实例化出来的对象不包含成员函数,定义类的成员函数都存放在公共的代码段并不是存放在对象中,不然创建多个对象会有很多个重复成员函数,实际应该是多个对象都调用的是同一个成员函数。
cpp
class stack
{
public:
void Init()
{
;
}
void Push()
{
;
}
private:
};
int main()
{
stack s1;
cout << sizeof(s1) << endl;
return 0;
}
如果我们不定义成员变量那么计算的大小会是多少呢?会不会是0?
不会,如果这个对象的大小是0,那么定义很多个这样的对象,他们的地址也就无法区分开来了。所以不管是空类(什么成员都没有)或者是没有成员变量的类,他们的大小最小是1,这个一个字节的意义就是为了占位,占了一个地址,可以区分不同的对象 。
类的对象的大小计算是和结构体一样的,同样也是遵守内存对齐的。
总结,再来看下面这段代码中的类的大小分别是多少呢?
cpp
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
答案:A1创建出来的对象大小是4,A2和A3都是1,所以现在我们也可看出来,对象中不包含成员函数。
关于结构体内存对齐和大小计算
结构体内存对齐规则
- 第一个成员在地址偏移量为0的地址处。
- 每个成员的第一个字节存放的位置的地址偏移量必须是对齐数整数倍(对齐数:编译器默认对齐数和类型大小之间的较小值)
- 默认对齐数(VS中默认为8,默认参数一般设置成1,2,4,8,16。可以通过#pragme pack (4),将编译器的默认对齐数修改为4。如果要恢复编译器默认对齐数,用#pragma pack()即可)
- 结构体整体的大小必须是,最大对齐数的整数倍(最大对齐数就是结构体中每个成员对齐数的最大值)
- 如果结构体内嵌套了其他结构体变量,那么这个结构体变量也要对齐到其对齐数的整数倍位置,这个结构体变量的对齐数就是它这个结构体的最大对齐数。
注意:只有vs有默认对齐数,其他编译器都是以自身类型大小作为自己的对齐数。
cpp
#pragma pack (8)
struct test2
{
char c;
double a;
};
int main()
{
stack s1;
cout << sizeof(struct test2) << endl;
return 0;
}
这段程序可以很好的说明最大对齐数,当默认对齐数为1的时候,这大小是9
因为默认对齐数是1,和变量类型中最大的8取较小值就是1.
当默认对齐数为4的时候这个结构体大小为12,8和4取较小的取到了4.
当默认对齐数是8,就没区别了,默认和最大都是8,那结果是16.
**【面试题】 **
- 结构体怎么对齐? 为什么要进行内存对齐
- 如何让结构体按照指定的对齐参数进行对齐
- 如何知道结构体中某个成员相对于结构体起始位置的偏移量
- 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
1 ,为什么要进行内存对齐,这是一种用空间换时间的做法,比如32位机有32根地址线,这样cpu每次读取都可以读取到四个字节,如果结构体成员都堆在一起放置,那么cpu每次读到的数据可能并不是只有一个数据,还可能截断读取了其他数字的部分数据,下次读的时候还需要分两次读取,效率就会很慢,若内存是对齐的,每次读取的数据都是一个数字的数据,就不需要cpu再去处理数据的问题了。
百度百科对此解释:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
3,如何知道结构体成员的偏移量呢?
使用宏offsetof(注意这是个宏不是函数),引用的头文件是
cpp
#pragma pack (8)
#include<cstddef>
struct test2
{
char c;
double a;
};
int main()
{
stack s1;
//cout << sizeof(struct test2) << endl;
cout << offsetof(struct test2, a)<<endl;
return 0;
}
this 指针
cpp
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;
d1.Init(2022, 6, 17);
d1.Print();
return 0;
}
下面我们来思考问题,当我们用不同的对象都调用Print函数和Init函数的时候,这个函数是如何区分形参要赋值给哪一个对象的成员变量呢?
这就是我们要说的this指针
cpp
//其实我们在调用Init的时候看似是这样传参
d1.Init(2022,6,17);
//实际上是
d1.Init(&d1,2022,6,17);
d1.Print();
d1.Print(&d1);
在传参的时候实际上还将对象的地址传了过去,只是这些是编译器自己处理的我们看不到而已。下面再来看函数部分是如何实现的。
cpp
//实际上的Init是这样的
void Init(Date*const this,int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
//Print也是如此
void Print(Date*this)
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
就像是C语言中的结构体指针,通过箭头来访问结构体内的对象,这个this指针就是同这种方式访问到每个对象的成员变量。
但是参数上的Data * const this是编译器隐含加上的,不可以手动加上,否则在传参的时候就会出现对应不上的问题。但是我们是可以直接在这个成员函数内使用this指针的,比如下面这样:
cpp
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
总结:
C++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性:
1,this指针的类型是:Date* const this,因为this指针是不可修改的所以用const保护起来。
2,this指针只能在非静态成员函数中使用。
3,this指针是成员函数的一个形参,是对象在调用函数的时候将对象的地址当作实参传递的。所以this指针并不在对象中,而是在存在栈区里面(this指针就是一个指针,一个局部变量)。
4,this指针是成员函数第一个隐含的指针形参,vs编译器通过ecx寄存器自动传递,不需要用户传递。
cpp
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = NULL;
p->PrintA();
p->Show();
}
这段代码可以编译通过,但是执行的时候会挂掉,p->PrintA的时候并没有对空指针进行解引用,虽然这个p指针是空指针,但是调用PrintA函数的时候只是把p当作参数传给了this指针,因为PrintA并不在对象中,所以也不需要对p解引用到对象里面找这个函数,而是需要到公共代码段内找这个函数,因此是可以编译通过的。
程序挂掉的原因是在PrintA中访问_a,实际上是this->_a。这里对空指针进行了解引用,所以程序崩溃了,如果屏蔽掉PrintA则程序没有问题。
注意:PrintA和Show函数的地址并没有存在对象里面。这些成员函数都是存在公共代码段的。