C++类和对象(一):inline函数、nullptr、类的定义深度解析

前言:

前面的博客给大家介绍了C++入门的知识点以及代码应用,接下来的文章就会进入到类和对象了,本篇文章会讲述C++入门的最后两个知识inline函数、nullptr,还有类和对象中的类的定义进行了解及应用,下面我们一起进入到文章中学习吧~

目录

一、inline内联函数

1.为什么要用inline函数?

1)坑1:

2)坑2:

3)坑3:

宏函数的正确写法:

2.正确使用inline函数:

3.默认debug版本下,为了方便调试,inline也不展开。要想展开,我们需要完成两设置:

4.inline只是一个建议,展开还是不展开由编译器说的算,递归和代码多的函数可能就会不展开:

5.inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错

二、nullptr

三、类的定义:

1.类定义格式:

2.访问限定符:

class类:

struct类:

3.类域:

一、inline内联函数

• 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。

• inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。

• C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。

• vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。

• inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

1.为什么要用inline函数?

因为在C语言中的宏函数有很多坑,用的时候很复杂易出错,所以C++用inline函数代替宏函数。

inline函数的好处:编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。

下面来看看宏函数的坑:

1)坑1:

cpp 复制代码
#define ADD(a,b) return a+b;
//错误,宏是一个替换机制,不是函数的写法

这个是最离谱的错误写法,直接把宏写成普通函数的形式了,记住:宏是一个替换机制,不是函数的写法。

2)坑2:

cpp 复制代码
#define ADD(a,b) a + b;
//错误,宏函数后面不能加分号;况且在调用宏函数时写入的是表达式,会出现运算符优先级的问题
int main()
{
	int ret1 = ADD(1, 2);
	//如果仍然带着分号,这个表达式展开就会是:
	int ret1 = 1 + 2;;  //会出现两个分号。
	return 0;
}

问题一是不能加分号,在去掉分号后仍然有错误:

cpp 复制代码
#define ADD(a,b) a + b
int main()
{
	int ret1 = ADD(1, 2);
	//这里没有问题
	int ret2 = ADD(1, 2) * 3;//但是这里就出现问题了
	//原本应该计算3*3=9,这里是1+2*3 = 7,由于优先级,所以结果不同
	return 0;
}

所以,宏函数的表达式应该带上括号 ( a + b )。

3)坑3:

cpp 复制代码
//#define ADD(a,b) (a + b)
//错误,调用该宏的时候写入的是表达式,仍然会出现运算符优先级的问题,如:
#define ADD(a,b) (a + b)
int main()
{
	int ret2 = ADD(1, 2) * 3;
	cout << ret2 << endl;//这里就没问题了,结果为9

	//但是在下面的场景仍然会出错:
	int x = 0, y = 1;
	int ret3 = ADD(x | y, x & y);
	cout << ret3 << endl;//仍然会出现运算符优先级的问题
	//展开之后:int ret3 = x | y + x & y; 
	//+号的优先级高于 | 和 & 所以这里先执行y+x,跟我们的想法不符
	return 0;
}

宏函数的正确写法:

cpp 复制代码
//正确写法:
#define ADD(a,b) ((a) + (b))
int main()
{
	int ret1 = ADD(1, 2);
	cout << ret1 << endl;

	int ret2 = ADD(1, 2) * 3;
	cout << ret2 << endl;
	
	int x = 0, y = 1;
	int ret3 = ADD(x | y, x & y);
	cout << ret3 << endl;
	//这次这三个结果都正确,符合预期
	return 0;
}

宏函数这么复杂,容易写出问题,还不能调试。

那我们为什么还要用它呢,它的优势在于什么呢?
优点:高频调用小函数时,写成宏函数,可以提高效率,预处理阶段宏会替换,提高效率,不建立栈帧。

2.正确使用inline函数:

cpp 复制代码
//在C++中我们觉得宏函数太麻烦,所以使用inline内联函数代替宏
//正确使用inline函数:
inline int Add(int a, int b)
{
	return a + b;
}

3.默认debug版本下,为了方便调试,inline也不展开。要想展开,我们需要完成两设置:

cpp 复制代码
#include<iostream>
using namespace std;
 
//转反汇编看,发现还是有call------还是创建了栈帧,这是为什么
inline int ADD(int a, int b)
{
	return a + b;
}
//因为默认debug版本下,为了方便调试,inline也不展开
//我们需要设置一下,这里大家可以自己测试看看
int main()
{
	int ret2 = ADD(1, 2) * 3;
	cout << ret2 << '\n';
 
	return 0;
}

设置步骤:

1.右键单击解决方案资源管理器中的项目,选择"属性 "。

2.在弹出的属性对话框中,找到"C / C++ "选项卡,点击"常规 "。

3.在"调试信息格式 "下拉菜单中,选择"程序数据库(/ Zi) "。

4.接着点击"C / C++"下的"优化 "选项。

5.在"内联函数的扩展 "下拉菜单中,选择"只适用于_inline(/ Ob1)"

4.inline只是一个建议,展开还是 不展开由编译器说的算,递归和代码多的函数可能就会不展开:

cpp 复制代码
#include<iostream>
using namespace std;

inline int ADD(int a, int b)
{
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	a += 1;
	//5个的时候还是可以展开的,10个就不再展开了
	return a + b;
}

int main()
{
	int ret2 = ADD(1, 2) * 3;
	cout << ret2 << '\n';

	return 0;
}

那么,为什么inline内联函数只是一个建议呢?

这是因为如果过多的代码被展开就会出现代码指令膨胀的问题,会导致可执行程序(安装包)过大,这是非常不好的。下面给大家举个例子:

5.inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错

这里我们拿顺序表为例:

SeqList.h(因为inline内联函数不能声明和定义分离,所以就都写到.h文件中)

cpp 复制代码
#pragma once
#include<iostream>
#include<stdlib.h>

typedef struct SeqList
{
	int* arr;
	int size;
	int capacity;
}SL;

inline void SLInit(SL& pls, int n = 4)
{
	pls.arr = (int*)malloc(n * sizeof(int));
	pls.size = 0;
	pls.capacity = n;
}
void SLPushBack(SL& pls, int x);
int SLFind(SL& pls, int x, int i = 0);
int& SLat(SL& pls, int i);
void SLModify(SL& pls, int i, int x);

SeqList.cpp(这里的SLInit就给注释了)

cpp 复制代码
#include"SeqList.h"

//void SLInit(SL& pls,int n)
//{
//	pls.arr = (int*)malloc(n * sizeof(int));
//	pls.size = 0;
//	pls.capacity = n;
//}

void SLPushBack(SL& pls, int x)
{
	//...
	pls.arr[pls.size++] = x;
}

int SLFind(SL& pls, int x, int i)
{
	while (i < pls.size)
	{
		//...
	}
	//...
	return -1;
}

int& SLat(SL& pls, int i)
{
	//...
	return pls.arr[i];
}

void SLModify(SL& pls, int i, int x)
{
	//...
	pls.arr[i] = x;
}

test.cpp

cpp 复制代码
#include"SeqList.h"

int main()
{
	SL s;
	SLInit(s); // call 地址
	
	return 0;
}

如果声明和定义分离到两个文件,就会报如下的错误:

二、nullptr

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

cpp 复制代码
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

• C++中NULL可能被定义为字面常量 0,或者C中被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于NULL被定义成0,调用了 f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。
• C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。

下面给大家用代码演示一下:

cpp 复制代码
#include<iostream>
using namespace std;

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

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

int main()
{
	f(0);
	f(NULL);
	//f((void*)0);//编译报错:error C2665: "f": 2 个重载中没有⼀个可以转换所有参数类型
	//用上面的都会执行出来函数1,而不会是函数2

	f(nullptr);//但是用nullptr就很清晰了,可以很好处理这个问题
	
	//原来在C语言中初始化的指针
	int* p1 = NULL;
	char* p2 = NULL;
	
	//以后我们在C++里面置为空都这样写
	int* p3 = nullptr;
	char* p4 = nullptr;

	return 0;
}

三、类的定义:

1.类定义格式:

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员;类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。

• 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_ 或者 m 开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。

• C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,一般情况下我们还是推荐用class定义类。

• 定义在类里面的成员函数默认为inline

2.访问限定符:

• C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

public 修饰的成员在类外可以直接被访问;protectedprivate修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。

• 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。

• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

• 一般成员变量 都会被限制为private/protected ,需要给别人使用的成员函数 会放为public

下面我们用代码示例演示一下:(详见注释)

class类:

cpp 复制代码
#include<iostream>
using namespace std;

// C++中数据和方法封装放到了一起,都在类里面
// 封装的本质体现了更严格的规范管理

class Stack
{
	//访问限定符,class中没给的话里面默认是私有
public://公有,一般是把成员函数公有,在类外也可以直接访问
	//成员函数
	void Init(int capacity = 4)//可以不用像之前一样前面加个类似ST的区分
	{
		_arr = nullptr;//这里本来是要malloc的
		_top = 0;
		_capacity = 0;
	}

	void Push(int x)
	{
		//。。。
	}
//public这个访问限定符到这里结束
private://私有,一般是把成员变量私有,在类外不能直接访问,更规范。
	//成员变量
	int* _arr;
	int _top;
	int _capacity;
};
//分号不能掉
//private后面没有访问限定符了,到 } 这里结束

//以前C语言中只能向上找,类中向上向下都可以。
//所以我们前面先实现公有的成员函数,再定义的私有里面的成员变量也是可以的

int mian()
{
	Stack s1;
	s1.Init();
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);

	//像之前C语言中这样不规范的写发,我们通过C++的访问限定符规范了
	//s1.top++;//私有成员,类外无法直接访问

	return 0;
}

为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如 _ 或者 m 开头:

int _yearyear_ 或 m_year 或 mYear...
补充小知识点:

1.面向对象三大特性:封装,继承,多态

2.C++中两种比较规范的写法:
驼峰法:

StackInit 函数 ,开头单词首字母大写开头 + 后续每个单词首字母都大写
initCpacity 变量 ,开头单词首字母小写开头 + 后续每个单词首字母大写
Google C++风格:
stack_init 函数 init_capacity 变量------都是单词之间用下划线分隔

struct类:

cpp 复制代码
#include<iostream>
using namespace std;

// C++升级struct升级成了类
// 1、类里面可以定义函数
// 2、struct名称就可以代表类型

//兼容C中struct的用法,升级成了类,就算不typedef也可以不用带前面的struct了
typedef struct AA
{
	void func1()
	{
	}

	int a1;
	int a2;
}AA;

struct BB
{
public:
	void func2()
	{
	}
private:
	int b1;
	int b2;
};

int main()
{
	//struct A a1;//不需要这样写了
	AA aa2;

	BB bb1;
	bb1.func2();

	return 0;
}

一般情况下我们更喜欢用class,但是像链表定义节点这种还是比较喜欢用struct的(默认公有):

cpp 复制代码
struct ListNode
{
	int val;
	//struct ListNode* next;
	ListNode* next;//可以直接这样写
};

3.类域:

• 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
• 类域影响的是编译的查找规则,下面程序中 Init 如果不指定类域Stack,那么编译器就把 Init 当成全局函数,那么编译时,找不到 array 等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道 Init 是成员函数,当前域找不到的 array 等成员,就会到类域中去查找。

SeqList.h

cpp 复制代码
#pragma once
#include<iostream>

class Stack
{
public:
	void Init(int capacity = 4);
	void Push(int x);
private:
	int* _array;
	int _top;
	int _capacity;
};

SeqList.cpp

cpp 复制代码
#include"SeqList.h"


//一定要带Stack::去找
void Stack::Init(int capacity)
{
	_array = nullptr; // malloc
	_top = 0;
	_capacity = capacity;
}
void Stack::Push(int x)
{
	//...
}

test.cpp

cpp 复制代码
#include<iostream>
#include"SeqList.h"
using namespace std;

int main()
{
	Stack s1;
	s1.Init();
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);

	return 0;
}

结语:

本篇文章就到此结束了,主要介绍了inline函数、nullptr、类的定义三大部分,接下来我们会继续学习类和对象的知识,学习任重而道远,欢迎大家继续关注。如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。

相关推荐
独自破碎E2 小时前
Java的CMS垃圾回收流程
java·开发语言
oioihoii2 小时前
C++线程编程模型演进:从Pthread到jthread的技术革命
java·开发语言·c++
2501_941322032 小时前
道路检测新突破:Cascade R-CNN在COCO数据集上的实战应用详解
开发语言·r语言·cnn
且去填词2 小时前
深入理解 GMP 模型:Go 高并发的基石
开发语言·后端·学习·算法·面试·golang·go
韩师学子--小倪2 小时前
JVM SafePoint
jvm
哪有时间简史2 小时前
Python程序设计基础
开发语言·python
Elcker2 小时前
JAVA-Web 项目研发中如何保持团队研发风格的统一
java·前端·javascript
zh_xuan2 小时前
kotlin对集合数据的操作
开发语言·kotlin
Hcoco_me2 小时前
大模型面试题76:强化学习中on-policy和off-policy的区别是什么?
人工智能·深度学习·算法·transformer·vllm