一. 面向过程和面向对象初步认识
C语言基于的是面向过程,**关注的是过程,**分析出求解问题的步骤,通过函数调用逐步解决问题。
C++基于的是面向对象,**关注的是对象,**将一件事拆分成不同的对象,靠对象之间的交互完成。
二. 类的引入
在C语言中,结构体中只能定义变量,但在C++中,结构体内不仅可以定义变量,还可以定义函数。
比如C语言实现栈,结构体中只能定义变量
cpp
struct Stack
{
int* a;
int top;
int capacity;
};
int main()
{
struct Stack st1;
return 0;
}
C++兼容C语言,所以在C++中,struct升级成了类,即可以定义变量,也可以定义函数 。并且结构体名称可以做类型
cpp
struct Stack
{
//成员函数
void Init(int defaultCapacity = 4) //申请空间
{
a = (int*)malloc(sizeof(int) * capacity)
if(nullptr == a)
{
perror("malloc申请空间失败");
return 0;
}
capacity = defaultCapacity;
top = 0;
}
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
struct Stack st1; //C语言写法
Stack str2; //类名就可以做类型 C++写法
str2.Init(); //调用 C++写法
return 0;
}
但上面结构体的定义,在C++中更喜欢用class来代替
三. 类的定义
代码语法
cpp
class className
{
//类体:由成员变量和成员函数组成
}; //注意后面的分号
其中class为定义类的关键字,className为类的名字,{}中为类的主体,注意定义结束时加上后面的分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量,类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1、声明和定义全部放在类体中。需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2、声明放在头文件(.h)中,定义放在源文件(.cpp)中。 在类体外定义成员,需要使用**::** 作用域解析符指明成员属于哪个类域。
在类中,一般会在成员变量前面会加入 "_" , 这是由于 C++ 有一个惯例,成员变量通常前面加 _变量 , 以便于区分 类函数中的形参变量。
cpp
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
// 所以一般都建议这样
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
四. 类的访问限定符和封装
4.1 访问限定符
访问限定符说明:
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到}类结束
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意: 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
举例对访问限定符加深理解:
public修饰的成员在类外可以直接被访问
cpp
//public修饰的成员在类外可以直接被访问
class Student
{
public: //全部都是公有
char _name[5];
char _gender[2];
int _age;
void Init(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Print()
{
cout << _name << " " << _gender << " " << _age << endl;
}
};
int main()
{
Student st1;
st1.Init("小雷","男",18); //成功运行
return 0;
}
protected和private修饰的成员在类外不能直接被访问
cpp
//protected和private修饰的成员在类外不能直接被访问
class Student
{
private: //成员变量私有
char _name[5];
char _gender[2];
int _age;
public: //成员函数公有 //protected与其类似
void Init(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Print()
{
cout << _name << " " << _gender << " " << _age << endl;
}
};
int main()
{
Student st1;
st1.Init("小雷","男",18); //正常运行
cout << st1._name << endl; //运行报错:无法访问private成员(在"Student"类中声明)
return 0;
}
class的默认访问权限为private,struct为public(因为struct要兼容C)
cpp
//class的默认访问权限为private,struct为public(因为struct要兼容C)
class Student
{
char _name[5];
char _gender[2];
int _age;
void Init(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Print()
{
cout << _name << " " << _gender << " " << _age << endl;
}
};
int main()
{
Student st1;
st1.Init("小雷","男",18); //运行报错:无法访问 private成员(在"Student"类中声明)
return 0;
}
面试题:
C++中的struct和class的区别是什么?
C++需要兼容C语言,所以C++中的struct可以当成结构体去使用。此外,C++中的struct还可以用来定义类,和class定义类是一样的,区别是struct的成员默认访问权限是public,而class的成员默认访问权限是private。
4.2 封装
面试题:
面向对象的三大特性:
封装、继承、多态
在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
封装: 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理 :想想我们是如何管理陕西省的兵马俑的。我们若什么都不管,兵马俑就被随意破坏了。所以我们建立了一座房子将兵马俑封装 起来。但是我们封装的目的不是为了不给别人看,所以我们开放了售票通道,人们可以通过买票突破封装,在合理的监管机制下进去参观。
类也是一样,我们使用类将数据和方法都封装起来。不想对外开放的就用 protected/private 封装起来,用 public 封装的成员允许外界对其进行合理的访问。所以封装本质上是一种管理。
我们都清楚C语言的数据和方法是分离的 ,我们以栈为例:
cpp
//C语言的数据和方法是分离的
struct Stack
{
int* _a;
int _top; // 数据
int _capacity;
};
void StackInit(struct Stack* ps)
{}
void StackPush(struct Stack* ps, int x) // 方法
{}
int StackTop(struct Stack* ps)
{}
int main()
{
struct Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 3);
StackPush(&st, 5);
return 0;
}
像C语言这样数据和方法分离会存在一个巨大的问题:太过自由!我没办法去对它很好的管理,就比如我现在想访问栈顶的数据,如若是C语言,我们就可以这样写:
cpp
printf("%d\n", StackTop(&st)); //规范
printf("%d\n", st._a[st._top]); //不规范
按理来说,第一行的代码才是正确的访问形式,第二行代码直接用结构去操纵成员,虽然也可以,但是会涉及到一个问题:我对top到底是栈顶元素还是栈顶元素的下一个位置?
(如果初始化里的top给的是0,那st.top就是栈顶元素的下一个位置,会越界,如果是-1,那么刚好就是栈顶元素)此时就会出现误用。相反直接调用StackTop函数就完全不会出现这个问题,因为我在之前就已经处理好了,返回的值必是栈顶元素。
由此可见,C语言在这一方面太过自由了,接下来来看看C++是如何来解决的。
这就需要用到封装了。C++设计出了类,类里除了可以定义成员变量,还可以定义函数,所以我们就可以把上述栈的初始化,插入,取栈顶全放到类里,也就实现了数据和方法封装到一起。
cpp
//1、数据和方法封装到一起,类里面
//2、想给你自由访问的设计成公有,不想给你访问的设计成私有
class Stack
{
public:
void Init()
{}
void Push(int x)
{}
int Top(struct Stack* ps)
{}
private:
int* _a;
int _top;
int _capacity;
};
并且此段代码还实现了我想给你访问的就设计成公有,不想给你访问的就设计成私有。此时就完美避免了C语言过渡自由懒散的弊端,此时就不能再像C语言那样直接访问成员变量了,而只能用成员函数。
cpp
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
cout << st.Top() << endl;
//cout << st._a[st._top] << endl; //err
return 0;
}
C++通过强制的就能让用的人更规范。并且一般情况设计类,成员数据都是私有或保护,想给你访问的函数是公有,不想给你访问的是私有或保护。
综上,C++的封装本质上就是一种更严格的管理。
就好比如兵马俑一样,在C语言中,这些兵马俑就像是没有受到任何权限限制,也没有工作人员监管,供游客观赏时间长了,可能会被乱涂乱画或者被人偷走了都不知道,而在C++中,这些兵马俑就受到了保护,有工作人员的巡逻,有摄像头的抓拍,也有玻璃罩将其封装到安全范围内。
五. 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用"::"作用域解析符指明成员属于哪个类域。
cpp
class Person
{
public:
//显示基本信息
void ShowInfo();
private:
char* _name; //姓名
char* _sex; //性别
int _age; //年龄
};
//这里需要指定ShowInfo是属于Person这个类域
void Person::ShowInfo()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
六. 类的实例化
用类类型创建对象的过程,称为类的实例化
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
- 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
cpp
int main()
{
Stack str1; //类实例化对象或者叫对象定义 (建房子)
//不能,类访问top是声明,还没开空间,top不能存数据 (图纸不能住人)
Stack::top = 1;
return 0;
}
七. 类对象模型
7.1 如何计算类对象的大小
一个类当中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?类的大小又是如何计算的呢?
cpp
class Person
{
public:
//显示基本信息
void ShowInfo()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
public:
char* _name; //姓名
char* _sex; //性别
int _age; //年龄
};
7.2 类对象的存储方式猜测
猜测一:对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
猜测2: 只保存成员变量,成员函数存放在公共的代码段
对于上述两种存储方式,计算机是按照哪种方式来存储的,我们可以通过对下面的不同对象分别获取大小来进行分析:
cpp
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
A1 st1; //对A1类的实例化
A2 st2; //对A2类的实例化
A3 st3; //对A3类的实例化
cout << sizeof(st1) << endl; //运行结果:4
cout << sizeof(st2) << endl; //运行结果:1
cout << sizeof(st2) << endl; //运行结果:1
return 0;
}
通过我们刚才得知的结果来看,类对象的存储方式采用的是第二种方式。 **类大小的计算不考虑成员函数,只考虑成员变量。**就好比每栋房子里面都需要客厅,卧室,床,桌子。但是并不都需要篮球场,只需要放到公共的区域。成员函数会存放在一个公共的区域,需要调用就去公共区域找
**注意:**当类里没有成员变量时,至少会给它开一个字节大小的空间,这1个字节不是为例存储有效数据,而是为了占位,表示对象存在过
结论:
- 没有成员变量的类对象,编译器会给它们分配1字节占位,表示对象存在过。
- 一个类的大小,实际就是该类中"成员变量"之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
八. 结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
九. this指针