目录
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
一、类的定义
C语言中,结构体中只能定义变量,在C++中,类是一个类型,由两部分构成:
(1)成员变量(属性)
(2)成员函数(行为)(被当做内联函数处理)
C++中的struct兼容C的所有用法,同时C++中把struct升级成类。
Stack.cpp
#include <stdlib.h>
#include <iostream>
using namespace std;
typedef int STDataType;
struct Stack
{
//初始化
void StackInit(int initSize = 4)
{
a = (STDataType*)malloc(sizeof(STDataType) * 4);
size = 0;
capacity = initSize;
}
STDataType* a;
int size;
int capacity;
};
int main()
{
Stack st;
//struct Stack st; C++兼容c的语法,对struct进行了升级,也可以加struct关键字定义对象
st.StackInit();
return 0;
}
C++中的类用class关键字表示,类有两种定义方式:
(1)声明和定义全部放在类体中,成员变量位置放在类中前面或后面都可以,如下所示,全部放在cpp文件中。
#include <stdlib.h>
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
//初始化
void StackInit(int initSize = 4)
{
a = (STDataType*)malloc(sizeof(STDataType) * 4);
size = 0;
capacity = initSize;
}
public:
STDataType* a;
int size;
int capacity;
};
int main()
{
Stack st; //定义一个对象
//struct Stack st; C++兼容c的语法,对struct进行了升级,也可以加struct关键字定义对象
st.StackInit();
return 0;
}
(2)类的声明放在.h文件中,类的定义放在.cpp文件中
Stack.h
#pragma once
#include <stdlib.h>
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
//初始化
void StackInit(int initSize = 4);
public:
STDataType* a;
int size;
int capacity;
};
Stack.cpp
#include "Stack.h"
void Stack::StackInit(int initSize = 4)
{
a = (STDataType*)malloc(sizeof(STDataType) * 4);
size = 0;
capacity = initSize;
}
int main()
{
Stack st;
st.StackInit();
return 0;
}
一般情况下,更期望采用第二种方式。
C++中struct和class的区别:
C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class的成员默认访问方式是private。
因此将struct改成class后,需要给成员变量和成员函数加上public访问限定符。
二、类的访问限定符和封装
1.类的访问限定符
用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将类的接口提供给外部用户使用。 C++的访问限定符分为以下几种:
(1)public修饰的成员在类外可以直接被访问
(2)protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
(3)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
(4)class的默认访问权限为private,struct为public(因为struct要兼容C)
注意: 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
2.类的封装
面向对象有3大特性:封装、集成、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:比如管理兵马俑,可以建一座房子把兵马俑给封装起来,不让别人看,开放售票通道,可以买票突破封装在合理的监管机制下进去参观。类也一样,使用类数据和方法都封装起来。不想给别人看的,使用protected/private把成员封装起来。开放一些公有的成员函数对成员合理的访问。所以封装本质是一种管理。
封装使面向对象更严格、更规范。
三、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。整个域是一个整体,成员变量无论在成员函数上面或者下面都可以进行访问。
#include<stdlib.h>
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
//初始化
void StackInit(int initSize);
public:
STDataType* a;
int size;
int capacity;
};
//指明成员属于Stack类
void Stack::StackInit(int initSize = 4)
{
a = (STDataType*)malloc(sizeof(STDataType) * 4);
size = 0;
capacity = initSize;
}
四、类的实例化
类的实例化:用类型创建对象的过程。
为什么类要进行实例化?
(1)类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
(2)一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
(3) 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
#include <stdlib.h>
#include<iostream>
using namespace std;
typedef int STDataType;
struct Stack
{
//初始化
void StackInit(int initSize = 4)
{
a = (STDataType*)malloc(sizeof(STDataType) * 4);
size = 0;
capacity = initSize;
}
STDataType* a;
int size;
int capacity;
};
int main()
{
Stack st; //实例化对象
return 0;
}
五、类对象模型
1.如何计算类对象的大小
遵循内存对齐原则,Stack类的成员变量是 a、size、capacity,每个变量的类型都是int,因此Stack类的成员变量大小为3*4=12个字节。
int main()
{
Stack st;
cout << sizeof(st) << endl;
return 0;
}
但是,打印发现Stack类的的对象大小就是12,那成员函数没有计算大小吗?
2.类的对象存储方式
假如对象中包含类的各个成员,既包含成员变量,又包含成员函数,由于每个对象中成员变量是不同的,但调用的是同一份成员函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份成员函数代码,相同代码保存多次,会浪费空间。
因此C++采用类对象中只存储成员变量,成员函数存放在公共代码段中的方式来存储类的对象。
#include<iostream>
using namespace std;
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
A1 a1;
A2 a2;
A3 a3;
cout << sizeof(a1) << endl;
cout << sizeof(a2) << endl;
cout << sizeof(a3) << endl;
cout << &a1 << endl;
cout << &a2 << endl;
cout << &a3 << endl;
return 0;
}
sizeof(a2) 和 sizeof(a3) 为什么都是1?编译器给空类一个字节来标识这是一个空类,这1个byte不是为了存储数据,是占位,表示对象存在过。尽管对象a2的类中没有成员变量仅有成员函数,对象a3的类中既没有成员变量也没有成员函数,但是它们的地址不同,表明对象a2和对象a3存在过。
六、this指针
当定义一个对象时,如何将对象初始化呢?C++把成员变量和成员函数封装到类里面,不想让成员变量随意被访问,需要通过公有的合法方式进行访问。下面的代码是无法给d1正确初始化的:
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d1;
//d1.year = 2022;//非合法访问,类外面无法访问private成员变量
d1.Init(2021, 5, 20);
return 0;
}
因为第9行year = year中的year都是第7行的入参year,因为所有的变量访问都遵循就近原则,类里面有两个变量year,一个是第8行的入参year,另一个是第13行的成员变量year,因此第9行代码中的赋值的两个year会优先访问第7行的入参year,都是第7行的入参year的值,把自己赋值给自己,没起任何作用。
虽然第9行可以指定域:
Date::year = year;
编译能够通过,但是写起来不方便,为此可以用命名风格进行区分,成员变量使用这样的风格:
private:
int _year;
int _month;
int _day;
也可以使用其他风格,只要和入参能够区分开就可以。因此,代码可以这样写:
#include<iostream>
using namespace std;
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(2022, 4, 8);
return 0;
}
F10监视对象d1的三个变量,发现被初始化成功了:
成员变量声明是没有为对象开辟空间的,只有定义了对象后,数据存储在对象里面
private:
//成员变量声明
int _year;
int _month;
int _day;
如果定义两个对象,d1和d2如何访问自己的年月日呢?两个对象调用的是同一份成员函数,如何做到各自初始化各自的?
int main()
{
Date d1;
d1.Init(2022, 4, 8);
Date d2;
d2.Init(2022, 4, 9);
return 0;
}
因为编译器做了处理,Init函数的入参不是3个参数,实际上是4个参数,编译器会增加一个隐含的参数:this指针,编译器会把对象的地址传给this。
int main()
{
Date d1;
d1.Init(2022, 4, 8);//实际为d1.Init(&d1,2022,4,8)
Date d2;
d2.Init(2022, 4, 9);//实际为d2.Init(&d2,2022,4,9)
return 0;
}
编译器增加的隐含的this指针参数,即void Init(Date* this, int year, int month, int day),作为形参,this指针存在于栈中。
this指针是隐含的,是编译器编译时显示自动增加的,不能显式地在调用和函数定义中增加,在Date类中第9-11行加不加this访问成员变量都能成功初始化,可以在成员函数中使用this指针。
使用this指针打印两个对象各自的地址:
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "this:" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 4, 8);
cout << "d1:" << &d1 << endl;
Date d2;
d2.Init(2022, 4, 9);
cout << "d2:" << &d2 << endl;
return 0;
}
第一次调用Init,this的地址就是d1的地址,第二次调用Init,this的地址就是d2的地址:
为了加深对this指针的理解,可以看看下面代码:
#include<iostream>
using namespace std;
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
p->Show();
}
程序在26行p->PrintA( );处崩溃,可以注掉第26行,就可以打印Show()了。
为什么访问p->PrintA( )会崩溃?因为p为空指针,第10行的成员函数PrintA()接收到的指针是空指针,访问this->_a对空指针解引用会崩溃。
p->Show( )没有对p这个指针解引用,因为Show( )这个成员函数的地址没有存到对象里面,因此能够正常编译。