C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
#include <iostream>
using namespace std;
//类和对象
//1个类 实例化 N个对象
//C++把struct升级成了类
//1、类里面可以定义函数
//2、struct名称就可以代表类型
//C++兼容C中struct的用法
typedef struct ListNodeC
{
struct ListNodeC* next;
int val;
}LTNode;
struct ListNodeCPP
{
ListNodeCPP* next;
int val;
};
struct Stack
{
//成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
//成员变量
int* array;
size_t capacity;
size_t top;
};
int main()
{
struct Stack st1;
st1.Init(100);
Stack st2;
st2.Init();
LTNode node1;
ListNodeCPP node2;
return 0;
}
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
面试题:
C++中 struct 和 class 的区别是什么?
C++需要兼容C语言,所以C++中 struct 可以当成结构体使用;另外C++中 struct 还可以用来定义类,和 class 定义类是一样的,区别是 struct 定义的类默认访问权限是 public ,class 定义的类默认访问权限是 private 的。(在继承和模板参数列表位置,struct 和 class 也有区别,后续再介绍)
面向对象的三大特性:封装、继承、多态
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
#include <iostream>
#include <assert.h>
using namespace std;
class Stack
{
public:
//成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
void Push(int x)
{
//扩容
//...
array[top++] = x;
}
int Top()
{
assert(top > 0);
return array[top - 1];
}
private:
//成员变量
int* array;
size_t capacity;
size_t top;
};
//封装
int main()
{
class Stack st1;
Stack st2;
st2.Init();
st2.Push(1);
st2.Push(2);
st2.Push(3);
st2.Push(4);
//cout << st2.array[st2.top] << endl;//err
cout << st2.Top() << endl;
return 0;
}
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到_a等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的_a等成员,就会到类域中去查找。
//Stack.h
class Stack
{
public:
void Init();
void Push(int x);
private:
int* _a;
int _top;
int _capacity;
};
class Queue
{
public:
void Init();
void Push(int x);
};
//Stack.cpp
#include "Stack.h"
void Stack::Init()
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
void Stack::Push(int x)
{
}
void Queue::Push(int x)
{
}
//class Date
//{
//public:
// void Init(int year, int month, int day)
// {
// //这样写编译能通过,但是没有初始化上,因为局部优先,局部没有才会到类域里去找
// year = year;
// month = month;
// day = day;
// }
//
//private:
// int year;
// int month;
// int day;
//};
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;//year_ m_year
int _month;
int _day;
};
int main()
{
Date d;
d.Init(2024, 3, 31);
return 0;
}
类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?
首先,函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。
再分析一下,对象中是否有存储指针的必要呢,Stack实例化 st1 和 st2 两个对象,st1 和 st2 都有各自独立的成员变量存储各自的数据,但是 st1和 st2的成员函数指针却是一样的,存储在对象中就浪费了。如果用 Stack 实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。因此,函数指针是不需要存储的。
函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解。
和C语言中的结构体一样,类对象的大小也符合内存对齐的规则
//Stack.h
class Stack
{
public:
void Init();
void Push(int x);
private:
int* _a;
int _top;
int _capacity;
};
//Stack.cpp
#include "Stack.h"
void Stack::Init()
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
//Test.cpp
#include "Stack.h"
#include <iostream>
using namespace std;
int main()
{
//类 -> 对象 1 -> 多
//类的实例化
Stack st1;
Stack st2;
st1.Init();
st2.Init();
//对象占用空间的大小,只考虑成员变量
cout << sizeof(st1) << endl;//12
cout << sizeof(Stack) << endl;//12
return 0;
}
#include <iostream>
using namespace std;
//空类 -> 1
//这一个字节,不存储有效数据,标识对象被定义出来了
class A3
{
};
//1byte
class A2
{
public:
void f2()
{
}
};
int main()
{
cout << sizeof(A3) << endl;//1
A3 aa1;
A3 aa2;
cout << sizeof(A2) << endl;//1
return 0;
}
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 和 Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?
那么这里就要看到C++给了一个隐含的this指针来解决这里的问题,即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问;只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
//注释掉的部分就是编译器的处理
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//void Print(Date* const this)
void Print()
{
//this = nullptr;//err
//cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;//年
int _month;//月
int _day;//日
};
int main()
{
Date d1;
Date d2;
d1.Init(2024, 4, 2);
d2.Init(2024, 4, 3);
//d1.Print(&d1);
d1.Print();
//d2.Print(&d2);
d2.Print();
return 0;
}
首先,就算是空指针访问也不是编译错误;Print()函数不在对象里面,也就是说不需要在对象里找函数的地址,这里p调用的意义是:第一,说明它是A类型的指针,就去A里面找这个成员函数(但是函数地址是在公共代码区里找);第二,传值给隐含的this(如果是对象,就把对象的地址传给this,如果是指针就把指针传给this),传一个空指针并不会报错,只有用空指针进行访问才会报错。因此,本题选C。
函数里的 _a 实际上是 this->_a ,这里用空指针进行访问,所以会运行崩溃。因此,本题选B。
this指针是一个形参,所以它是存在栈上的(有些地方它是存在ecx寄存器上的,因为只要用到成员变量就要用到this指针,所以this指针会被频繁使用,而寄存器的特点就是很快,因此这相当于是一种优化)