【C++学习】内联函数 | nullptr空指针 | 初步认识面向对象 | 类访问限定符 | 封装 | 类对象的内存对齐

写在前面:

上一篇文章我介绍了引用和auto相关的知识,

如果有兴趣的话可以去看看:http://t.csdn.cn/j6jsI

这篇文章大概能够讲完C++入门的一些语法,开始类和对象的学习之旅。

目录

写在前面:

1. 内联函数

2. nullptr空指针

3. 初步认识面向对象

4. 类的引入

5. 类访问限定符

6. 封装

7. 类对象的内存对齐

写在最后:


1. 内联函数

我们先来看这样一个情况:

#include <iostream>
using namespace std;

int Add(int x, int y) {
	return x + y;
}

int main()
{
	for (int i = 0; i < 10000; i++) {
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

这段代码循环调用了一万次这一个Add函数,

创建销毁了一万次这个函数的函数栈帧,造成了大量的资源消耗,

我们该怎么解决这样的问题呢?

在学习C语言的时候,我们一般使用宏函数来解决这样的问题,

来看代码:

#include <iostream>
using namespace std;

#define Add(x, y) ((x) + (y))

int main()
{
	for (int i = 0; i < 10000; i++) {
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

使用宏函数本质上是一种替换,

将 Add ( i, i + 1 ) 替换成 ( ( i ) + ( i + 1 ) )

宏函数有他的优点也有缺点,

优点:不需要建立栈帧,提高调用效率;

缺点:复杂,容易出错,可读性差,不能调试,没有类型安全的检查。

C++就想着新增一种方法来解决这样的问题,减少宏函数的缺点,也就是内联函数。

内联函数的用法很简单,就是在函数前面加一个关键字:inline

#include <iostream>
using namespace std;

inline int Add(int x, int y) {
	return x + y;
}

int main()
{
	for (int i = 0; i < 10000; i++) {
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

加上inline 之后函数就会变成内联函数,

内联函数会在调用的地方展开,这样就没有函数调用了,

这就是内联函数,他不需要建立栈帧,提高了效率,

不复杂,不容易出错,可读性强,可以调试,几乎是一招解决了所有问题。

那你可能会问,如果内联函数这么牛逼,我们能不能把所有函数都搞成内联呢?

但是宏函数和内联函数都有一个适用场景,

他们适用于短小的频繁调用的函数,太长的是不适合的,会导致代码膨胀的问题。

实际上,内联仅仅只是一个建议,最终是否是内联,是编译器自己决定的。

一般而言,比较长的函数是不会成为内联的,一般递归也不会成为内联。

另外,在默认的debug模式下,一般是不支持内联的。

补充:如果要用内联,就别搞声明和定义分离,直接写就行了。

2. nullptr空指针

C语言已经有NULL了,为什么C++还要添加nullptr呢?

来看这段代码:

#include <iostream>
using namespace std;

void f(int x) {
	cout << "f(int x)" << endl;
}

void f(int* x) {
	cout << "f(int* x)" << endl;
}

int main()
{
	f(0);
	f(NULL);

	return 0;
}

NULL代表的是空指针,我们用NULL想调用第二个函数,但是,

这段代码的输出:

f(int x)
f(int x)

为什么会出现这样的情况?

我们可以看看NULL的底层是怎么样的:

他实际上就是宏定义出来的0,

我们来看nullptr:

#include <iostream>
using namespace std;

void f(int x) {
	cout << "f(int x)" << endl;
}

void f(int* x) {
	cout << "f(int* x)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	f(nullptr); // #define nullptr ((void*)0)

	return 0;
}

 输出:

f(int x)
f(int x)
f(int* x)

他调用的就是第二个函数,

为什么呢?

其实就是因为nullptr的类型是 void*,算是给NULL打的一个补丁,

所以我们以后一般尽量都使用nullptr就行。

3. 初步认识面向对象

用一个经典的例子来解释面向对象和我们之前学习的面向过程的区别:

比如说一个外卖系统:

如果是用面向过程的思想解决:

就可以分成几个步骤:上架,点餐,派单,送餐等等。

如果是用面向对象的思想解决:

就可以分成几个对象:平台,商家,骑手,用户等等。

面向对象的优势是可以在同一个抽象的系统中实例化出多个对象,

关注的是对象和对象之间的关系和交互。

这些听起来很抽象,慢慢理解就行。

4. 类的引入

其实我们之前C语言就有结构体这一种自定义类型,

到了C++,结构体就被升级成了类,来看例子:

#include <iostream>
using namespace std;

//并且在C/C++里面用{}括起来的位置都是一个域,这里是就是类域
struct Stack {
	//成员函数(类内可以放成员函数)
	void Init() {
		//...
	}

	void Push() {
		//...
	}

	//...等等

	//成员变量
	int* a;
	int top;
	int capacity;
};

// C++兼容C语言,struct以前的用法都能继续用
// 而struct升级成了类,类的类名能直接当类型使用
int main()
{
	struct Stack st1;
	Stack st2;

	//调用的时候就可以这样调用
	st2.Init();
	st2.Push();

	return 0;
}

其实C++更喜欢使用的是class,也就类,class和struct基本上没什么区别,

当我们把struct改成class之后:

发现编译器报错了,这是为什么?

这里就要说到另一个知识点。

5. 类访问限定符

C++给出了三种访问限定符:

public(公有)

private(私有)

protected(保护)

而公有表示的是能在类外面访问,私有和保护表示的是不能在类外面访问。

私有和保护在平时的使用上是一样的,只有在继承的时候有所区别。

这个时候就能回答为什么改成class之后编译器会报错了,

因为struct类域默认是公有,而class的类域默认是私有。

(这个设计其实就是为了兼容C语言)

那么回归正题,平时我们在定义的类时候,

我们习惯将成员变量放在private私有,将成员函数放在public公有。

说人话就是:我想给你用的就放公有,不想让你碰到的就放在私有:

#include <iostream>
using namespace std;

//并且在C/C++里面用{}括起来的位置都是一个域,这里是就是类域
class Stack {
public:
	//成员函数(类内可以放成员函数)
	void Init() {
		//...
	}

	void Push() {
		//...
	}

	//...等等
		 
private:
	//成员变量
	int* a;
	int top;
	int capacity;
};

// C++兼容C语言,struct以前的用法都能继续用
// 而struct升级成了类,类的类名能直接当类型使用
int main()
{
	struct Stack st1;
	Stack st2;

	//调用的时候就可以这样调用
	st2.Init();
	st2.Push();

	return 0;
}

这个时候又出现了新的问题,

来看下面这段代码:

#include <iostream>
using namespace std;

class Date {
public:
	void Init(int year) {
		year = year;
	}

private:
	int year;
};

int main()
{
	Date d;
	d.Init(2023);

	return 0;
}

这段代码中 Init 函数里面的 year = year ,你知道是谁赋值给谁吗?

编译器并没有报错,跑过了,

这里我再讲的清楚一点,这两个year究竟是成员变量还是函数形参?

因为这样的原因,我们一般习惯给成员函数加一点标记:

#include <iostream>
using namespace std;

class Date {
public:
	void Init(int year) {
		_year = year;
	}

private:
	int _year;
};

int main()
{
	Date d;
	d.Init(2023);

	return 0;
}

 给成员函数前面加上一个_ ,我习惯这样区分,其实还有其他的区分方式,

每个人又不一样的代码风格,这个就看个人喜好或者是其他的需求了。

这里是没有硬性的要求的。

6. 封装

其实我们将给别人用的成员函数放在公有,

把成员变量放在私有,其实这就是封装思想的一种体现。

封装是什么?

将数据和数据的方法有机结合,隐藏对象的属性和实现细节,

仅对外公开接口来和对象进行交户的行为就是封装。

封装的本质其实是一种更好的管理。

这里补充一句:类内的成员函数默认都是内联函数。

7. 类对象的内存对齐

我们在学习C语言结构体的时候,曾经学过结构体的内存对齐,

那么下面这个类的内存对齐是多少呢?  

#include <iostream>
using namespace std;

class Stack {
public:
	void Init() {
		//...
	}

	void Push() {
		//...
	}

	//...等等
		 
private:
	//成员变量
	int* a;
	int top;
	int capacity;
}; 

int main()
{
	Stack st1;
	cout << sizeof(st1) << endl;

	return 0;
}

输出:

12

是的,你没有看错,类的内存对齐计算方法是跟结构体一模一样的,

而且,类的成员函数是不被计算在内的。

这个时候你可能会有疑问,为什么类的成员函数没有被计算在内?

来看这样一个例子:

#include <iostream>
using namespace std;

class Stack {
public:
	void Init() {
		//...
	}

	void Push() {
		//...
	}

	//...等等
		 
//private:
	//成员变量
	int* a;
	int top;
	int capacity;
}; 

int main()
{
	Stack st1;
	st1.top = 0;
	st1.Init();

	Stack st2;
	st2.top = 10;
	st2.Init();

	return 0;
}

我将类内成员的访问限定设置成公有,

那么 st1 的成员变量 top 跟 st2 的成员变量 top 是同一个变量吗?

显然不是,他们有着各自独立的空间,存放着不同的数据,

那么,st1 调用的 Init 函数和 st2 调用的 Init 函数他们调用的是同一个函数吗?

实际上,他们调用的就是同一个函数,

不然要是每实例化一个新的对象就要拷贝一份成员函数,那浪费的资源可太多了。

那问题来了,这个函数他存放在哪里呢?为什么两个对象都能调用的到?

这个问题就由我下一篇文章再来揭晓了。

写在最后:

以上就是本篇文章的内容了,感谢你的阅读。

如果感到有所收获的话可以给博主点一个哦。

如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~

猜你喜欢

转载自blog.csdn.net/Locky136/article/details/131373344
今日推荐