目录
1.面向对象和面向过程
首先我们知道,C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
而C++是基于面向对象的,关注的是对象本身,将一件事情拆分成不同的对象,靠对象之间的交换完成。
2.类的引入
C语言结构体中只能定义变量,在C++中,结构体中不仅可以定义变量,还可以定义函数。比如:学数据结构时,用C语言方式实现的栈,结构体中只可以定义变量;现在以C++方式实现,会发现在 struct中也可以定义函数。
如下:
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_arr = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _arr)
{
perror("malloc file");
exit(1);
}
_capacity = capacity;
_top = 0;
}
void Push(const DataType& data)
{
_arr[_top] = data;
++_top;
}
DataType Top()
{
return _arr[_top - 1];
}
void Destroy()
{
if (_arr)
{
free(_arr);
_arr = nullptr;
_capacity = 0;
_top = 0;
}
}
DataType* _arr;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st;
st.Init(4);
st.Push(1);
st.Push(2);
st.Push(3);
cout << st.Top() << endl;
st.Destroy();
return 0;
}
上面的结构体的定义,在C++中更偏向用class来替代。
3.类的定义
写法:
class className
{
//类型:由成员函数 和 成员变量组成
}; //后面一定要加上分号!
class为定义类的关键字,ClassName为类的名字,{ } 中是类的主体,注意类定义结束时后面的分号是不能省略的。
类体中内容称为类的成员:类中的变量称为 类的属性 或 成员变量;类中的函数称为类的方法 或者 成员函数。
类的两种定义方式:
1.声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可以会将其当成内联函数处理。
2.类声明放在 .h 文件中,成员函数定义放在.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.类的访问限定符与封装
4.1 访问限定符
C++实现封装的方式:用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用使户用。
【访问限定符说明】
1.public 修饰的成员在类外可以直接被访问。
2.protected 和 private 修饰的成员在类外不能直接被访问(此处protected 和 private 是类似的)。
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
4.如果后面没有访问限定符,作用域就到 } ,也就是类结束。
5.class的默认访问权限为private,struct 为 public(因为struct 要兼容C语言)。
注意:访问限定符只在编译时有效,当数据映射到内存后,没有任何访问限定符的区别。
4.2 封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
分组本质上是一种管理,让用户更方便使用类。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
5.类 -- 作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Student
{
public:
//显示学生的基本信息
void StudentInfo();
private:
char* _name;//姓名
char* _sex;//性别
int* _age;//年龄
char* _tal;//电话
};
//定义StudentInfo函数时,需要指定它是属于Student这个类域
void Student::StudentInfo()
{
cout << _name << "-" << _sex << "-" << _age << "-" << _tal << endl;
}
6.类 -- 实例化
用类 类型 创建对象的过程,称为类的实例化。
1.类 是对 对象进行描述的,是一种模型 一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
2.一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。
下面这种写法是错误的,要先把类实例化成对象,再进行访问类中的成员。
int main()
{
Studnet.name = "张三";
return 0;
}
Student类是没有空间的,只有Student类实例化出的对象才有具体的属性(成员)。
7.类对象模型
7.1 计算类对象的大小
思考:类中有成员变量和成员函数,那一个类的对象中包含了什么?一个类的大小是怎么计算的?
class A
{
public:
void PrintA()
{
cout << _c << endl;
}
private:
char _c;
};
7.2 类对象的存储方式
设计一:对象中包含类的各个成员(成员变量 与 成员函数)。
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照这种方式存储,当一个类创建多个对象时,每个对象都会保存一份代码,相同代码保存多次,很浪费空间,那么要怎么解决?
设计二:代码只保存一份,在对象中保存存放代码的地址。
设计三:只保存成员变量,成员函数存放在公共的代码段。
上述三种存储方式,计算机使用哪种方式进行存储?
我们再通过下面不同对象分别获取大小来分析(三种情况):
类中既有成员变量又有成员函数:
class A1
{
public:
void func1(){}
private:
int _a;
};
类中仅有成员函数:
class A2
{
public:
void func2(){}
};
类中既没有成员变量也没有成员函数 -- 空类
class A3
{};
计算机是使用第三种方式进行存储类对象的,只保存成员变量,成员函数存放在公共的代码段。
所以上述类对象的大小分别是:
sizeof(A1):4, sizeof(A2) : 1 , sizeof(A3):1
所以这里的A2和A3的大小是一样的,因为成员函数是存储在公共代码区,不存储在对象中。
总结:一个类的大小,实际就是该类中 ”成员变量“ 之和,当然要注意内存对象。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
7.3 结构体内存对齐规则
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS 中默认对齐数是 8.
3.结构体总大小为:最大对齐数(所有的变量类型最大者 和 默认对齐参数取最小)的整数被。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
8.this指针
8.1 this指针
定义一个日期类Date:
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;
Date d2;
d1.Init(2024, 3, 2);
d2.Init(2024, 2, 23);
d1.Print();
d2.Print();
return 0;
}
对于上述类,有这样一个问题:
Date类中有Init 和 Print 两个成员函数,函数体中没有关于不同对象的区分,那么当d1 调用Init 函数时,该函数是如何知道应该设置 d1 对象,而不是设置 d2 对象呢?
C++ 中通过引入 this 指针 解决这个问题。即:C++编译器给每个 ”非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
8.2 this指针 -- 特性
1.this 指针的类型:类类型* const,即成员函数中,不能给this指针赋值;
2.只可以在 “成员函数” 的内部使用;
3.this指针本质上是 “ 成员函数 ”的形参, 当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4.this指针是 “成员函数” 第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
看看下面这两段程序的编译结果是什么?
1.下面程序编译运行结果是 ? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
分析:上述这段程序编译是 正常运行的,虽然p指向了一个空指针,并且让p指向了类成员函数 ,但是类成员函数并不是存储在对象中,而是存储在公共代码段,所这里p->Print()时,并没有发生解引用.所以这里是可以正常运行的。在VS中执行 p->_a;(这里什么也没有做) 时也不会报错,是因为VS对它进行了优化,在别的编译器上运行可能会报错,但是p->_a = 1; 这里对_a进行了解引用,程序会崩溃。
(*p).Print(); 和 p->Print(); 的运行结果时一样的,可以看一下反汇编指令。这里只要做两件事,一是传递this指针,二是call这个函数的地址。
2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
分析:上述这段代码运行会崩溃。调用Print函数时,会对_a进行访问,此时this->_a为空,进行解引用程序崩溃。
8.3 对比C++ 与 C语言 实现Stack的方式
C语言实现
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int DateType;
typedef struct Stack
{
DateType* arr;
int capacity;
int top;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->arr = (DateType*)malloc(sizeof(DateType) * 3);
if (NULL == ps->arr)
{
perror("malloc file");
return;
}
ps->capacity = 3;
ps->top = 0;
}
void StackDestroy(Stack* ps)
{
free(ps->arr);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
void CheckCapacity(Stack* ps)
{
if (ps->top == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
DateType* tmp = (DateType*)realloc(ps->arr,sizeof(DateType) * newCapacity);
if (NULL == tmp)
{
perror("realloc申请空间失败!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
void StackPush(Stack* ps,DateType data)
{
assert(ps);
CheckCapacity(ps);
ps->arr[ps->top] = data;
ps->top++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->top;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->top--;
}
DateType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->arr[ps->top - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
int main()
{
Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
printf("%d\n", StackTop(&st));
printf("%d\n", StackSize(&st));
StackPop(&st);
StackPop(&st);
printf("%d\n", StackTop(&st));
printf("%d\n", StackSize(&st));
StackDestroy(&st);
return 0;
}
3
3
1
1
可以看到,使用C语言实现时,Stack相关操作函数有以下共性:
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数检测,因为该参数可能为NULL
- 函数中都是通过Stack*参数操作栈的
- 调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相对复杂一点,因为涉及大量指针操作,稍不注意就可能会出问题。
2、C++实现
typedef int DataType;
class Stack
{
public:
void Init()
{
_arr = (DataType*)malloc(sizeof(DataType) * 4);
if (NULL == _arr)
{
perror("malloc file");
exit(1);
}
_capacity = 4;
_top = 0;
}
void Push(DataType data)
{
CheckCapacity();
_arr[_top] = data;
_top++;
}
int Empty()
{
return 0 == _top;
}
DataType Top()
{
return _arr[_top - 1];
}
int size()
{
return _top;
}
void Pop()
{
if (Empty())
return;
--_top;
}
void Destroy()
{
if (_arr)
{
free(_arr);
_arr = nullptr;
_top = _capacity = 0;
}
}
private:
void CheckCapacity()
{
if (_top == _capacity)
{
int newCapacity = _capacity * 2;
DataType* tmp = (DataType*)realloc(_arr, newCapacity * sizeof(DataType));
if (tmp == NULL)
{
perror("realloc申请空间失败");
exit(1);
}
}
}
private:
DataType* _arr;
int _top;
int _capacity;
};
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
st.Push(3);
printf("%d\n", st.Top());
printf("%d\n", st.size());
st.Pop();
st.Pop();
printf("%d\n", st.Top());
printf("%d\n", st.size());
st.Destroy();
return 0;
}
运行结果:
3
3
1
1
C++中通过类可以将数据 以及 操作数据的方式进行完美配合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知性。并且每个方法不需要传递Stack* 的参数了,编译器编译之后该参数会自动还原,即C++中Stack*参数是编译器在维护的,但是C语言需要由用户自己维护。