上期C++教程:超级详细的C++教程4:内联函数、auto关键字、范围for、nullptr
1.初步认识面向过程和面向对象
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,提供函数的调用逐步解决问题;
下面是用C语言的思想来解决洗衣服的过程:
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成;
C++实现洗衣服总共分为4个对象:人,衣服,洗衣粉,洗衣机;人将衣服和洗衣粉放入到洗衣机里面去。洗衣机是如何去洗衣服的过程我们不需要关注,只需要调用洗衣机这个对象即可。
总结:
面向过程关注的是过程,分析出求解问题的步骤,提供函数的调用逐步解决问题;
面向对象关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成;
2.C++结构体的升级
C++兼容C语言结构体的所有用法,同时还将结构体升级成了类。
类有两大特点:1.类名就是类型,2.类里可以定义函数
1.类名就是类型
假设我们要定义一个链表的节点,C语言的方法,例如:
struct ListNode
{
struct ListNode* next;
int a;
};
int main()
{
struct ListNode* s;
return 0;
}
C++将结构体升级后,可以将struct省略不写,因为类名就是类型,C++定义链表节点的方法,例如:
struct ListNode
{
ListNode* next;
int a;
};
int main()
{
ListNode* s;
return 0;
}
2.类里面可以定义函数
C++里结构体里面是可以写函数的,例如:
struct stack
{
int* data;
int top;
int capacity;
void Init()
{
data = nullptr;
top= 0;
capacity = 0;
}
};
C++结构体 VS C语言结构体
假如我们要实现一个栈,C语言的方法是这样的,例如:
//数据
struct stack
{
int* data;
int top;
int capacity;
};
//方法
void StackInit(struct stack* ps)
{
ps->data = NULL;
ps->capacity = 0;
ps->top= 0;
}
void StackPush(struct stack* ps,int x)
{
if(ps->capacity == ps->top)//扩容
{
//....
}
ps->data[ps->top] = x;
ps->top++;
}
C语言面向的是过程,它关注的是过程,所以它的数据和方法是分离的。
如果要使用上面C语言实现的栈,例如:
int main()
{
struct stack s;
StackInit(&s);
StackPush(&s,10);
StackPush(&s,20);
StackPush(&s,30);
return 0;
}
C++实现栈的方法,例如:
struct stack
{
int* data;
int top;
int capacity;
void Init()
{
data = nullptr;
top= 0;
capacity = 0;
}
void Push(int x)
{
if(capacity == top)//扩容
{
//....
}
data[top] = x;
top++;
}
};
使用上面C++所实现的栈,例如:
int main()
{
stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
return 0;
}
3.类的定义
虽然C++将struct升级成了类,但是C++喜欢用class定义类,例如:
class CalssName
{
//类体:由成员函数和成员变量组成
};//后面一定要有分号,和结构体一样
class为定义类的关键字, ClassName为类的名字, {}中为类的主体,注意类定义结束时后面分号不能省略;类体中内容称为类的成员:类中的变量称为成员变量; 类中的函数称为成员函数(需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理);
使用class定义类和struct定义类不同,程序会出现变化,这里就引入了新的东西---->访问限定符。
1.访问限定符
类的访问限定符有3个,分别是:public(公有),protected(保护),private(私有);
class Date
{
public://下面的成员在类外面可以被访问
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d._year = 2024;
d._month = 11;
d._day = 13;
d.PrintfDate();
return 0;
}
#include <iostream>
using namespace std;
class Date
{
private://下面的成员在类外面不可以被访问
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d;
//错误代码///
d._year = 2024;//不能在类外面被访问
d._month = 11;//不能在类外面被访问
d._day = 13;//不能在类外面被访问
d.PrintfDate();//不能在类外面被访问
///
return 0;
}
#include <iostream>
using namespace std;
class Date
{
public://下面的成员在类外面可以被访问
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private://下面的成员在类外面不可以被访问
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.PrintfDate();//可以在类外面访问
//错误代码///
d._year = 2024;//不能在类外面被访问
d._month = 11;//不能在类外面被访问
d._day = 13;//不能在类外面被访问
///
return 0;
}

#include <iostream>
using namespace std;
struct LsitNode
{
int a;
LsitNode* next;
};
class stack
{
int* a;
int top;
int capacity;
};
int main()
{
//struct的默认权限是公有,可以在类外面访问
LsitNode a;
a.a = 10;
a.next = nullptr;
//class的默认权限是私有,不可以在类外面访问
stack s;
s.a = nullptr;//错误代码,a是私有成员,不可以在类外面访问
s.top= 0;//错误代码,top是私有成员,不可以在类外面访问
s.capacity = 0;//错误代码,capacity是私有成员,不可以在类外面访问
return 0;
}
struct里面也可以使用访问限定符来限制成员的访问权限,例如:
class stack
{
public:
void Init()
{
a = nullptr;
size = 0;
capacity = 0;
}
private:
int* a;
int size;
int capacity;
};
总结:
1.在类里面,如果我们想别人使用我们就定义成public(公有),不想让别人访问就定义成protected(保护)和private(私有),如果我们不写访问限定符,class里面默认是private(私有);
2.struct里面也可以去使用访问限定符,如果我们不写访问限定符,struct里面默认是public(公有);
3.struct和class没有区别,只是默认访问限定符有区别,一般情况定义类都是使用class,只有极少数的情况下才会用struct。
2.类的作用域
#include <iostream>
using namespace std;
class person
{
public:
void PrintfPerson();
private:
char name[20];
int age;
char id[15];
};
void person::PrintfPerson()
{
cout << name << " ";
cout << age << " ";
cout << id << endl;
}
上面代码中,在PrintfPerson这个函数名前面加上person:: 是告诉编译器PrintfPerson这个函数是person这个类作用域里面的。
成员函数如果在类中定义,编译器可能会将其当成内联函数处理(我们可以认为前面有一个隐藏的inline),例如:
class person
{
public:
void PrintfPerson();
int GetAge()//编译器会将类里面定义的函数当成内联函数处理
{
return age;
}
private:
char name[20];
int age;
char id[15];
};
void person::PrintfPerson()
{
cout << name << " ";
cout << age << " ";
cout << id << endl;
}
虽然成员函数如果在类中定义,编译器可能会将其当成内联函数处理,但是它不一定会直接展开,还是得看编译器的最终决定(C++的语法细节超级多)。
C++类的规范写法:
1.短小的函数可以在类里面直接定义;
2.递归或者长函数在类里面声明,类外面定义;
3.成员变量的前面或者后面最好加上一个_来和函数参数区分(这是一个良好的惯例);
根据上述,所演示的person类规范的写法,例如:
#include <iostream>
using namespace std;
class person
{
public:
void PrintfPerson();
int GetAge()//编译器会将类里面定义的函数当成内联函数处理
{
return age;
}
private:
char _name[20];
int _age;
char _id[15];
};
void person::PrintfPerson()
{
cout << _name << " ";
cout << _age << " ";
cout << _id << endl;
}
3.类的实例化
我们先来定义一个日期类Date,例如:
class Date
{
public:
void init(int year,int month,int day);
private:
int _year;
int _month;
int _day;
};
void Date::init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
上面代码中,Date类的成员_year、_month、_day这3个变量是声明还是定义????
函数的声明和定义很好区分,如上面的Date类的init函数:
而上面代码中,Date类中_year、_month、_day这三个成员变量是声明,而不是定义。因为这三个成员变量没有在内存中开辟空间,它只告诉你这三个变量的名字和类型分别是什么。
只有定义后才会在内存中开辟空间,例如:
int main()
{
Date d;
return 0;
}
上面代码中d就是个对象(d既可以叫对象,也可以叫变量,C++面向对象,更喜欢叫对象),而在d这个对象被定义出来后_year、_month、_day这三个成员变量作为d这个对象内的成员才会定义出来。
这句代码就是对象的定义,也叫做对象的实例化。
类和对象的关系是一对多,一个类可以有多个对象。我们可以把类看作是房子的设计图,把对象看作是房子,一张房子的设计图是可以造出很多的房子。
4.类对象模型
class A
{
public:
void init(int a, char b, int c)
{
_a = a;
_b = b;
_c = c;
}
void print()
{
cout << _a << " " << _b << " " << _c <<endl;
}
private:
int _a;
char _b;
int _c;
};
成员函数不在类里面,如果要计算一个类的大小,只计算成员变量的大小,还要考虑内存对齐。所以上面代码中A这个类的大小为12个字节。
不同对象使用的是同一个成员函数,如下图所示:
从上图中可以看出,a1和a2这两个对象分别调用的init和print这成员函数都是同一个成员函数,因为函数的地址都是一样的。
所以成员函数不在对象里,而是在公共代码区。而成员变量a1和a2各自都有一份,因为用同一个成员函数所打印出来的结果不同。
所以对象的模型是这样的,如下图所示:
一个类所实例出来的多个对象,成员变量每个对象各自都有一份,每个对象共同一起使用同一个成员函数。
一个没有成员的类叫空类,它的大小为1个字节,这个字节不存储有效数据,它标识定义的对象存在过,例如:
#include <iostream>
using namespace std;
class B
{};
int main()
{
cout << sizeof(B) << endl;
return 0;
}
只有成员函数,没有成员变量的类的大小也是一个字节,例如:
#include <iostream>
using namespace std;
class C
{
public:
void func()
{}
};
int main()
{
cout << sizeof(C) << endl;
return 0;
}
5.this指针
我们先来定义一个日期类Date,例如:
#include <iostream>
using namespace std;
class Date
{
public:
void init(int year,int month,int day);
void print();
private:
int _year;
int _month;
int _day;
};
void Date::init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::print()
{
cout << _year << "-";
cout << _month << "-";
cout << _day << endl;
}
int main()
{
Date d1;
Date d2;
d1.init(2024,11,4);
d2.init(2024,11,5);
d1.print();
d2.print();
return 0;
}

对象调用成员函数会被编译器处理成下图中的这个样子,如图所示:
所以这就是为什么d1调用 init 函数时,该函数设置的是d1对象,而不是设置d2对象,如下图所示:
也就是说哪个对象调用成员函数,this指针就会指向那个对象。
学到这里,回头再去看C++和C语言所实现的栈,其实底层是一样的,只是编译器默默为我们做了处理,如下图所示:
类里面的成员函数,每一个函数的第一个参数都是this指针(除了后面学的静态成员,成员函数的实际参数个数会比看到参数个数会多一个),哪个对象调用成员函数,this指针就会指向那个对象,this指针它是隐藏起来的,我们不能显示的去写this指针的形参和实参,否则编译器会报错,但是我们可以在类里面显示的去使用(一般不建议),例如:
class Date
{
public:
void init(int year, int month, int day);
void init(int year,int month,int day);
void print();
private:
int _year;
int _month;
int _day;
};
void Date::init(int year,int month,int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
int main()
{
Date d1;
d1.init(2024,11,4);//实际上的调
d1.print();
return 0;
}
this指针后面我们会用到它,一般都不会显示的去使用(只有新手才会显示的去写,因为这样代码会更清晰)。
this指针是不能被改变,但是它指向的内容可以被修改;
this指针没有存在对象里面,this是一个形参,它是存在栈帧上;
4.封装
面向对象的三大特性: 封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。
在C语言中,就没有访问限定符来对结构体进行一个封装,如果使用不规范会造成问题,例如有些网上的代码和书上的代码中C语言所实现的栈,明明提供了判断栈是否为空的函数,但是他们直接使用里面的成员top来对栈进行判空,例如:
struct Stack
{
int* a;
int top;
int capacity;
};
int main()
{
Stack a;
//有问题的代码,使用不规范
if(a.top == 0)//判断栈是否为空
{
//...
}
return 0;
}