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

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

写在前面:

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


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


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


目录


写在前面:


1. 内联函数


2. nullptr空指针


3. 初步认识面向对象


4. 类的引入


5. 类访问限定符


6. 封装


7. 类对象的内存对齐


写在最后:


1. 内联函数

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

#include 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 函数他们调用的是同一个函数吗?


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


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


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


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


写在最后:

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


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


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


相关文章
|
2天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
52 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
32 3
|
3月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
89 2
|
3月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
156 5
|
3月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
170 4