【C++】学习笔记——类和对象_2

简介: 【C++】学习笔记——类和对象_2

二、类和对象

7. 类对象的大小(补)

7.2 类的内存对齐规则

类的对其规则和结构体的内存对齐规则是一样的。

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整>数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

让我们来看看例子:

#include<iostream>
using namespace std;
class A
{
  int _a;
  char _b;
};
class B
{
  char _b;
  int _a;
};
int main()
{
  cout << sizeof A << endl;
  cout << sizeof B << endl;
  return 0;
}

结果是:

在类 A 中,_a 变量从偏移量为0开始存储,一共四个字节, _b 变量的对齐数为1,说明在1的倍数地方可以存储这个变量,总共占据了5个字节的空间,而类的总大小要是8的倍数,所以结果是8。

在类 B 中,_b变量从0开始,占1个字节,而 _a 变量的对其数为4,需要从4的倍数处才能存储,所以_a变量从4开始,存到7为止,这个类空间要是8的倍数,所以结果就是8。

我们会发现,内存对齐会导致内存浪费,为什么要内存对齐?

因为不是所有的硬件平台都能够访问任意地址的任意数据的,一般只能在某些地址处访问特定类型的数据,这回导致访问未对齐的数据时,需要从从前面访问一次,再从后面访问一次,需要两次访问,而对齐则仅需一次访问。本质上就是空间换时间的做法

8. this指针

8.1 this指针的引出

我们来先定义一个日期类:

#include<iostream>
using namespace std;
class Date
{
public:
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1, d2;
  d1.Init(1111, 1, 11);
  d2.Init(2222, 2, 22);
  d1.Print();
  d2.Print();
  return 0;
}

对于上面的类,我有这样一个问题:d1和d2调用的明明是同一个printf()函数,为啥输出的结果不一样呢,函数也不带参数,它是怎么区分d1的数据和d2的数据呢?

这下不得不牵扯出 this指针 了,C++的类中,所有的成员函数都隐含了一个参数,这个参数就是this指针。成员函数是通过this指针来访问相应对象的成员变量的。形式上就像:

// 成员函数
void print(Date* const this)
{
  cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
// 调用
d1.print(&d1);
d2.print(&d2);

this指针跟普通的参数没什么区别,只是C++将其给隐藏了。

8.2 this指针的特点

我们不能主动去将this指针给写出来,也不能修改this指针的指向,但是在函数内部是可以使用this指针的。

那么,this指针存放在哪里?因为this指针是形参,所以this指针存在栈区。this指针可以为nullptr吗?我们来看看下面两段程序。

程序1:

#include<iostream>
using namespace std;
class A
{
public:
  void Print()
  {
    // ↓ ↓ ↓
    cout << "Print()" << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->Print();
  return 0;
}

程序2:

#include<iostream>
using namespace std;
class A
{
public:
  void Print()
  {
    // ↓ ↓ ↓
    cout << _a << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->Print();
  return 0;
}

程序1和程序2分别运行后的结果是什么呢?答案是:程序1正常运行,程序2运行崩溃。为什么呢?因为尽管p指针是nullptr,但是在调用成员函数的时候,本质上是把p值传递给this指针,首先这一步肯定是没有问题的,在程序1中,函数仅仅打印了一段字符串,并没有使用this指针,所以this指针是空也可以正常运行,反观程序2,我们要去this指针的指向的空间里面找成员变量,但是this指针指向空,根本找不到成员变量,于是程序产生了崩溃。(注意: p->Print() 与 (*p).Print() 是一样的,结果相同。)this 指针可以为空

9. 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。默认成员函数就是你不写,编译器会自动生成的成员函数。

10. 构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值。(不是void空返回值,是无返回值,不需要写返回值)
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载

示例:

#include<iostream>
using namespace std;
class Date
{
public:
  // 构造函数
  Date()
  {
    _year = 1111;
    _month = 1;
    _day = 1;
  }
  // 重载构造函数
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  // 注意,这里d1的后面不能加(),否则难以跟函数声明区分开
  Date d1;
  d1.Print();
  Date d2(2222, 2, 2);
  d2.Print();
  return 0;
}

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

#include<iostream>
using namespace std;
class Date
{
public:
  // 没有显式定义构造函数
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

此时编译期生成的默认构造函数会做什么呢?

结果是啥也没做。那默认构造函数又有什么用呢,有何意义?其实是:默认构造函数对于内置类型(语言自身定义的类型,如:int, char)不做处理,自定义类型(自己定义的结构体或类)则调用自定义类型的默认构造函数

内置类型不做处理其实属于缺陷,如果也能处理则会更加方便,于是在C++11版本,C++委员会对这个语法打了补丁,允许内置类型在声明处给缺省值

#include<iostream>
using namespace std;
class Date
{
public:
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  // 声明处给缺省值
  int _year = 1111;
  int _month = 1;
  int _day = 1;
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

默认构造函数就是编译器自动生成的函数,这句话对吗?其实不对,编译器自动生成的构造函数确实是默认构造函数,但是无参的构造函数和全缺省的构造函数都属于默认构造函数。总结一下:不需要传参的构造函数就是默认构造函数。这三种函数只需要写出一个,建议是全缺省的构造函数。

11. 析构函数

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由

编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include<iostream>
using namespace std;
class Date
{
public:
  // 析构函数
  ~Date()
  {
    // ...
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year = 1111;
  int _month = 1;
  int _day = 1;
};
int main()
{
  Date d1;
  d1.Print();
  return 0;
}

日期类的析构函数没啥好写的,毕竟都是栈上的空间,函数结束后空间自动销毁。而在某些类里,有些变量在堆上开辟了一片空间,变量的生命周期到了后自动调用析构函数来将堆上的空间释放。在C语言里,空间是需要手动释放的,忘记释放则会造成内存泄露的问题,C++则会自动调用析构函数来释放空间

构造函数是先定义的先构造,而析构函数相反,最先定义的最后析构,就跟括号匹配一样。编译器默认生成的析构函数与构造函数一样,内置类型不做处理,自定义类型调用自定义类型的析构函数。

未完待续

目录
相关文章
|
10天前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
56 30
|
24天前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
24天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)
|
24天前
|
C++
C++(十六)类之间转化
在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。
|
24天前
|
存储 设计模式 编译器
C++(十三) 类的扩展
本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。
|
24天前
|
存储 C++
C++(五)String 字符串类
本文档详细介绍了C++中的`string`类,包括定义、初始化、字符串比较及数值与字符串之间的转换方法。`string`类简化了字符串处理,提供了丰富的功能如字符串查找、比较、拼接和替换等。文档通过示例代码展示了如何使用这些功能,并介绍了如何将数值转换为字符串以及反之亦然的方法。此外,还展示了如何使用`string`数组存储和遍历多个字符串。
|
1月前
|
存储 安全 编译器
【C++】类和对象(下)
【C++】类和对象(下)
【C++】类和对象(下)
|
1月前
|
存储 算法 编译器
c++--类(上)
c++--类(上)
|
1月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
1月前
|
编译器 C++
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决