目录
1:面向过程与面向对象的初步认识
在之前呢,我们有了解过,C语言是一门面向过程的编程语言,关注的是过程,分析求解出问题的的步骤,通过函数调用逐步解决问题.这就好比生活中我们去洗衣服,会经过如下步骤,每一步该干啥,然后逐步去解决问题,这就是C语言的特性!
而C++是基于C语言,是一门面向对象的编程语言,更侧重于对象,将一件事情拆分成不同的对象,靠对象之间的交互去完成这件事.还是拿洗衣服的例子来进行类比.
在洗衣服这件事中有几个对象呢?
总共有四个对象:人,衣服,洗衣机,洗衣粉.
如果按照C++的方式去理解这件事的话,那么整个洗衣服的过程:人将衣服放进洗衣机、倒入洗衣粉、启动洗衣机、洗衣机完成洗衣过程后并且进行甩干.
整个过程中主要是:人、衣服、洗衣服、洗衣机四个对象之间的交互,人不需要去关心洗衣机是如何洗衣服的以及是如何甩干的.
2.:类的引入
在C语言阶段,我们通过结构体与一些相关函数能够实现栈这一数据结构.
2.1:C语言版本
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//C语言实现栈
struct Stack
{
int* arr;
int top;
int capacity;
};
void Init(struct Stack* ps, int value)
{
int* tmp = (int*)malloc(sizeof(int) * value);
if (nullptr == tmp)
{
perror("malloc fail:");
return;
}
ps->arr = tmp;
ps->capacity = value;
ps->top = -1;
}
void Push(struct Stack* ps, int value)
{
//扩容....
ps->arr[++ps->top] = value;
}
int main()
{
Stack ps;
Init(&ps, 5);
Push(&ps, 1);
cout << ps.arr[ps.top] << " ";
Push(&ps, 2);
cout << ps.arr[ps.top] << " ";
Push(&ps, 3);
cout << ps.arr[ps.top] << " ";
Push(&ps, 4);
cout << ps.arr[ps.top] << " ";
}
上述代码则是通过C语言实现栈这一数据结构,我们可以清晰地发现,C语言的结构体中只能够定义变量,但是不能够定义函数,要将函数写在结构体的外面.那我们再来看看C++版本的栈.
2.2:C++版本
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//C++语言实现栈
struct Stack
{
int* arr;
int top;
int capacity;
void Init(int value)
{
int* tmp = (int*)malloc(sizeof(int) * value);
if (nullptr == tmp)
{
perror("malloc fail:");
return;
}
arr = tmp;
capacity = value;
top = -1;
}
void Push(int value)
{
//扩容....
arr[++top] = value;
}
};
int main()
{
Stack ps;
ps.Init(5);
ps.Push(5);
cout << ps.arr[ps.top] << " ";
ps.Push(6);
cout << ps.arr[ps.top] << " ";
ps.Push(7);
cout << ps.arr[ps.top] << " ";
ps.Push(8);
cout << ps.arr[ps.top] << " ";
}
我们可以发现,在C++中结构体不仅可以定义变量,还可以定义函数,这样子在使用时就更方便了,不过在C++中更多的是喜欢用class关键字来代替struct.
3:类的定义
C++中类的定义和C语言中结构体的定义方式是一样的,只是C++中更比较倾向于使用class关键字
cpp
class className
{
//类体:由成员函数以及成员变量构成.
};
class为定义类的关键字,className为类的名字,{}为类的主体
PS:和C语言一样,注意类定义结束时后面的分号不能省略哦~
类体中内容为类的成员 :类中的变量可以被称为类的属性 或成员变量; 类中的函数 被称为类的方法 或成员函数.
3.1:类的定义方式一(声明与定义全部放在类体中)
类有两种定义方式,第一种方式是声明与定义全部放在类体中;
PS:成员函数如果在类中定义,编译器可能会将其当成内联函数处理.
cpp
#define _CRT_SECURE_NO_WARNINGS
#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;
d1.Init(2022,3,4);
d1.Print();
}
上述方式则是定义类的第一种方式,声明与定义均在类体中.
在上面我们有讲到过,成员函数如果在类中定义,编译器可能会将其当成内联函数处理,那么我们来仔细观察下.
通过观察反汇编我们可以清晰地看到,Init()与Print()函数都是Date类的成员函数,Init()函数被编译器当成了内联函数进行处理,因此在调用时没有了call指令,而Print()函数则在调用时被call了,因此它没有编译器被当成内联函数.
3.2:类的定义方式二(声明与定义分离)
第二种方式定义类的方式那就是声明与定义分离了,不过这里有些小细节哦!
3.2.1:Date.h
cpp
#pragma once
class Date
{
public:
void Print();
void Init(int year,int month,int day);
private:
int _year;
int _month;
int _day;
};
3.2.2:main.cpp
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "Date.h"
void Date::Init(int year,int month,int day)
{
_year = year;
_month = month;
_day;
}
void Date::Print()
{
cout << _year << "年 " << _month << "月" << _day << "日";
}
int main()
{
Date d1;
d1.Init(2022, 3, 2);
}
使用声明与定义分离的方式去定义类时,定义成员函数时要使用域作用限定符指定类域,因为类定义了一个新的作用域,之前我们说过编译器的搜索原则:
不指定域:(1):先去当前的局部作用域进行搜索(2):全局作用域
指定域:若指定了域,就会直接去域里面进行搜索
包含头文件的本质是:将头文件里面所包含的内容拷贝过来.
所以这里uu们要注意下哦~
一般情况下,更希望采用第二种定义方式,不过博主为了方便演示则是使用了第一种定义方式,uu们下去自己练习的时候尽量使用第二种哦~.
3.3:成员变量命名规则的建议
有的uu就会很疑惑,为什么在声明成员变量的时候要带个_呢?好,我们来对比一下如果声明成员变量不带_.
都是成员变量,上面这两份代码中,很明显是左边的这个可读性要高些吧,至于为什么要带_,是为了和成员函数里面的形参进行区分,如果成员变量不带_的话,代码的可读性就会降低很多,不便于理解,因此通常在定义成员变量的时候,博主是比较习惯使用_的;除了_,其他方式也是可以的,uu们可以根据自己的习惯来哦~
4:类的实例化
概念:用类类型创建对象的过程,称为类的实例化.
uu们可能对上面这句话有点难以理解,举个简单的例子,在日常生活中,我们去建造一个房子,首先得需要一个设计图,去设计这个房子该怎么建造,然后再根据这个设计图去实际建造.在这个场景下,类就相当于是设计图,用类实例化出对象就像是生活中根据设计图建造出房子.在这一场景的基础上我们可以得知:
1:类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,因此定义一个类并没有分配实际的内存空间来存储,只是告知了该类里面有哪些成员.
2:一个类可以实例化出多个对象:实例出的对象,占用实际的内存空间,存储着成员变量.
可能uu们对上面的概念还是有些模糊,我们通过几段代码来看看.
4.1:代码1
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date._year;
return 0;
}
上面的代码中,只是定义了一个Date类,但是并没有实例化出对象,因此在访问Date类里面的成员变量时,是无法成功的.
4.2:代码2
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Print();
d2.Print();
return 0;
}
上面的代码中,则实例化出两个对象d1与d2,并且通过调试,我们可以清晰得观察到,此时d1与d2两个对象占据了实际的内存空间,里面并且存储着成员变量.
5:类的访问限定符
uu们在看上面的代码时候,会很疑惑,为什么在定义类的时候里面会有个public还有private呢?这是什么意思呢?这里就要讲到C++实现封装的方式了:用类将对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用.uu们可能对这句话还有些疑惑,我们通过代码来简单解析下.
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
void Print()
{
cout << "hello world" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1._year;
return 0;
}
当我们上面这段代码的时候,会发现报错了,这是因为类里面的成员变量还有成员函数是属于类里面的,不能够直接在类外面去访问.那么我们有什么办法去访问类里面的成员函数和成员变量呢?这里就要用到访问限定符了,C++中有如下三种访问限定符(protect访问限定符到后期再讲解,这里uu们简单了解下就好).
5.1:public修饰成员
使用public修饰的成员在类外可以直接被访问
代码1
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1._year;
d1.Print();
return 0;
}
当使用了public关键字修饰后,此时我们就能够在类外面访问Date类里面的成员啦.
5.2:private修饰成员
代码1
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
void Print()
{
cout << "hello world" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1._year;
d1.Print();
return 0;
}
当使用了private关键字修饰成员后,此时我们能够清晰地观察到,这个时候无法在类外面去访问类里面的成员了.
5.3:访问限定符的作用域
访问限定符是有作用域滴,其作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;如果后面没有访问限定符,作用域就到}结束.我们来看下面几段代码.
代码1
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1._year;
d1.Print();
return 0;
}
上面的代码中,由于public访问限定符往后都没有下一个访问限定符出现,所以此时整个类里面的成员都是公有的,能够在类外被访问.
代码2
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1._year;
d1.Print();
return 0;
}
上面的代码2中,由于public访问限定符往后有了private访问限定符,因此此时Date类里面的成员函数是公有的,能够在类外被访问,类里面的成员变量是私有的,不能够在类外被访问.
PS:
- class的默认访问权限为private,struct的默认访问权限为public(因为struct为了兼容C语言)
- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别.
5.4:封装
面向对象有三大特性:封装、继承、多态
在前期,我们更多的是涉及封装的特性,继承与多态的特性等到后期博主在具体讲解,那么什么是封装呢?
- 封装其实指的是将数据与操作数据的方法进行有机结合,隐藏对象的属性与实现细节,只对外开放接口来和对象进行相关的交互.
- 封装的本质其实是一种管控,让用户更加方便地去使用类.
6:类对象模型
6.1:类对象的大小
在C语言阶段,我们讲到过结构体的大小计算要遵循内存对齐的规则,这里我们简单回顾下
对齐规则:
1:第一个成员变量在结构体变量偏移量为0的地址处.
2:其他成员变量要对齐到对齐数的整数倍的地址处.
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值.
- VS中的默认的值为8
- Linux(gcc的编译器)中没有默认对齐数,对齐数就是成员自身的大小.
3:结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍.
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
struct s3
{
double d;
char c3;
int i;
};
int main()
{
cout << sizeof(s3) << endl;
return 0;
}
上述代码中的结构体大小为什么是16呢?我们来一步一步看
- 对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值.
(1):第一个成员变量在结构体变量偏移量为0的地址处,第一个成员变量为double类型,double类型占据8个字节,默认对齐数为8,所以对齐数为8,因此从0开始一直到7为d所占据的字节空间.
- 其他成员变量要对齐到对齐数的整数倍的地址处.
(2):第二个成员变量为char类型,char类型占据1个字节,默认对齐数为8,所以对齐数为1,0到7这块空间倍double类型所占据了,8为1的整数倍,因此变量c3在8这个位置占据1个空间.
(3):第三个成员变量为int类型,int类型占据4个字节,默认对齐数为8,所以对齐数为4,但是9,10,11这三个位置都不是对齐数4的位置,所以这三个字节会被浪费掉,从12开始往后4个位置则是i所占据的字节空间
- 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍.
(4):由于15这个不是最大对齐数8的整数倍,所以s3结构体还会再占用一个字节空间,因此结构体s3所占据的字节空间的大小为16.
上述方式则是C语言阶段计算结构体的大小方式,那么我们再回到类,类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么,该如何计算呢?
6.2:类对象的存储方式的猜测
6.2.1:对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同的代码保存多次,容易造成空间的浪费.
6.2.2:在对象中保存成员函数的地址
6.2.3:只保存成员变量,成员函数存放在公共的代码段
那么对于上面三种存储方式,计算机是按照哪种存储方式来存储的呢?我们来看下面几段代码
代码1(既有成员变量又有成员函数)
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
private:
int _year;
int _month;
};
int main()
{
Date d1;
cout << sizeof(d1) << endl;
return 0;
}
代码2(仅成员变量)
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
};
int main()
{
Date d1;
cout << sizeof(d1) << endl;
return 0;
}
代码3(仅成员函数)
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << "hello world" << endl;
}
};
int main()
{
Date d1;
cout << sizeof(d1) << endl;
return 0;
}
代码4(空类)
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
};
int main()
{
Date d1;
cout << sizeof(d1) << endl;
return 0;
}
通过观察上面的四种情况,我们能够得出一个结论:一个类的大小,实际就是该类"成员变量之和"(要注意内存对齐),也就是第三种存储方式,如果是第二种存储的方式的话,那么在计算的时候会算上这个地址的大小,而上面的结果并没有计算,同时通过观察反汇编可以看到,所调用的类的方法的地址是一样的,因此是第三种存储方式.
PS:对于空类,编译器给了一个字节来唯一标识这个类的对象
7:this指针的引出
cpp
#define _CRT_SECURE_NO_WARNINGS
#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;
d1.Init(2024, 1, 27);
Date d2;
d2.Init(2024, 1, 28);
d1.Print();
d2.Print();
return 0;
}
上面的Date类中有Init与Print两个成员函数,函数的形参中没有关于对不同对象的区分,那当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++中通过引入this指针来解决这个问题;C++编译器给每个 "非静态的成员函数(非静态后面再讲)"增加了一个隐藏的指针参数,让该指针指向该对象(函数运行时调用该函数的对象),在函数体中所有"成员变量"的操作,都是通过该指针去访问。只不过所有操作对用户来讲是透明的,即用户不需要来传递,由编译器来完成.uu们可以根据下面这张图去理解this指针
7.1:this指针的特性
- this指针的类型: * const,即在成员函数中,不能给this指针赋值.
- this指针只能在成员函数内部使用
cpp
#define _CRT_SECURE_NO_WARNINGS
#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 <<this-> _year << "/" << this->_month << "/" <<this-> _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 1, 27);
Date d2;
d2.Init(2024, 1, 28);
d1.Print();
d2.Print();
return 0;
}
- this指针的本质上是"成员函数"的形参,当对象调用成员函数时,将对象的地址作为实参传递给this形参.因此对象中不存储this指针.
- this指针存储在栈区里头,因为它是一个成员函数的第一个隐含的指针形参(有些编译器比如vs可能会用寄存器来进行存储)
7.2:常见例题
7.2.1:例题一
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
我们看上面这段代码,p是个对象指针,里面存储了对象的地址,并且赋予了空值,然后通过指针p对其解引用去访问成员函数,按照在C语言所学的知识,这段程序是会发生报错的,那么我们来运行看看.
运行出来,我们能够清晰地看到,没有报错,那么是为什么呢?在上面我们讲过,对象中只存储成员变量,成员函数而是放在了一块公共的代码区,而指针p存储的是对象的地址,那么在解引用访问的时候,访问的并不是对象所占据的空间,而是那段公共代码区,因此才能够运行成功.
发生空指针解引用的本质是:该指针对所指向的空间里头的成员进行了访问.
7.2.2:例题二
cpp
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
为什么代码2会发生报错呢,因为成员函数里头对对象里头的成员进行了访问因此发生了空指针的解引用.
7.2.3:例题三
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << this << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
上面这段代码为什么能正常运行呢?因为this指针是成员函数的形参,不存在于对象里头,因此没有发生空指针的解引用.
好啦,家人们,关于类和对象(上)这块的相关细节知识,博主就讲到这里了,如果uu们觉得博主讲的不错的话,请动动你们滴滴给博主点个赞,你们滴鼓励将成为博主源源不断滴动力!