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


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


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


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


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


写在最后:

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


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


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


相关文章
|
19天前
|
存储 C语言
指针和动态内存分配
指针和动态内存分配
74 0
|
8天前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
8天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)
|
12天前
|
存储 程序员 Python
Python类的定义_类和对象的关系_对象的内存模型
通过类的定义来创建对象,我们可以应用面向对象编程(OOP)的原则,例如封装、继承和多态,这些原则帮助程序员构建可复用的代码和模块化的系统。Python语言支持这样的OOP特性,使其成为强大而灵活的编程语言,适用于各种软件开发项目。
14 1
|
27天前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
27天前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
29天前
|
C++ 容器
C++中自定义结构体或类作为关联容器的键
C++中自定义结构体或类作为关联容器的键
30 0
|
29天前
|
存储 算法 搜索推荐
【C++】类的默认成员函数
【C++】类的默认成员函数
|
7天前
|
C++
C++(十六)类之间转化
在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。
|
8天前
|
存储 设计模式 编译器
C++(十三) 类的扩展
本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。