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


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


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


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


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


写在最后:

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


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


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


相关文章
|
25天前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
23天前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
104 0
|
2月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
85 19
|
2月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
79 13
|
2月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
68 5
|
2月前
|
存储 C++
【C++面向对象——输入输出流】处理二进制文件(头歌实践教学平台习题)【合集】
本任务要求使用C++读取二进制文件并在每行前添加行号后输出到控制台。主要内容包括: 1. **任务描述**:用二进制方式打开指定文件,为每一行添加行号并输出。 2. **相关知识**: - 流类库中常用的类及其成员函数(如`iostream`、`fstream`等)。 - 标准输入输出及格式控制(如`cin`、`cout`和`iomanip`中的格式化函数)。 - 文件的应用方法(文本文件和二进制文件的读写操作)。 3. **编程要求**:编写程序,通过命令行参数传递文件名,使用`getline`读取数据并用`cout`输出带行号的内容。 4. **实验步骤**:参考实验指
66 5
|
2月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
|
4天前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
JVM简介—1.Java内存区域
|
2天前
|
消息中间件 Java 应用服务中间件
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
|
4天前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略