C++:类和对象(中)---默认成员函数---运算符重载---const的含义

简介: C++:类和对象(中)---默认成员函数---运算符重载---const的含义

默认成员函数

首先要理解什么是默认成员函数:类在什么都不写的时,编译器会生成六个默认成员函数

用户没有显式实现,但编译器会生成的成员函数就是默认成员函数

下面我们对这些函数一一进行介绍

构造函数

在C语言中,无论是实现栈队列链表等各种数据结构,都避免不了要写Init初始化函数,这个函数的功能是给变量一个初始化的值,在C++中,认为C语言的这些问题有些许麻烦,于是进行了一定的优化,构造函数就是要在对象创建的时候,就把信息设置进去

构造函数是一个特殊的成员函数,名字和类名相同,创建类型对象的时候就由编译器自己自动调用,用来保证类中的每一个数据成员都有一个自己的初始值,在整个对象的生命周期中只调用一次

构造函数的特性

构造函数是特殊的成员函数,主要功能是用来初始化对象

它有下面的一些特点


  1. 函数名和类名相同
  2. 没有返回值
  3. 对象实例化的时候会自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式的构造函数,那么会自动生成一个无参的默认构造函数,如果用户定义了构造函数就不再生成
  6. 默认构造函数是在不写的时候会生成,且内置类型的成员不会进行处理(在C++11中的声明支持给缺省值),自定义类型的成员才会处理,回去调用这个成员的默认构造函数
  7. 无参的构造函数和全缺省的构造函数都是默认构造函数,这两个构造函数只能存在一个

既然它有这么多的特点,那么就一一举例论证它的特点

1. 函数名和类名相同
2. 没有返回值
3. 对象实例化的时候会自动调用对应的构造函数
4. 构造函数可以重载

在实际写代码时,尽量要写全缺省的构造函数,这样不管如何给参数都有一定的初始化值

class Date
{
public:
  Date(int year=1, int month=1, int day=1)
  {
    cout << "Date(int year, int month, int day)" << endl;
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  Date d2(2004);
  Date d3(2004, 6);
  Date d4(2004,6,16);
  return 0;
}

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

这里也是我们接触到的第一个默认构造函数,我们用下面的代码证明它的存在性

下面继续验证它的后半段存在性

从中可以看出,当我们没有定义构造函数时,定义一个无参的对象编译器会执行默认构造函数给数据成员初始值,而现在我们定义了构造函数,那么默认构造函数不复存在,此时编译器也就不能执行默认构造函数给无参的d1初始化值,此时会报错—Date不存在默认构造函数,没有合适的构造函数可以使用

6. 默认构造函数是在不写的时候会生成,内置类型的成员不会进行处理(在C++11中的声明支持给缺省值),自定义类型的成员才会处理,回去调用这个成员的默认构造函数

简单来说,一般情况下都需要我们自己写构造函数,决定初始化方式,成员变量都是自定义类型,可以考虑不写构造函数,因为会调用自定义类型的构造函数

7. 无参的构造函数和全缺省的构造函数都是默认构造函数,这两个构造函数只能存在一个

后续还有初始化列表,我们后续进行讲解

析构函数

析构函数就相对简单一点

它存在的意义是什么呢?

我们实现栈顺序表链表等数据结构时,都要在堆上malloc开辟一段空间,但是是不是经常会忘记free呢?这样会造成内存泄漏的危险情况的发生

那么C++在开发的时候就想到了这个问题,因此析构函数就这样产生了,它存在的意义就是完成对象中资源的清理工作,它会在对象被销毁前自动调用

析构函数的特点如下

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。析构函数不能重载
  4. 对象生命周期结束时,C++编译自动调用析构函数。
  5. 调用顺序满足栈的顺序

拷贝构造函数

在实际代码运用中,我们经常会拷贝一个对象用来做其他事情,在C语言中,这个过程十分简单,把值直接全部从一个内容中拷贝到另外一个地方即可,但在C++中却不那么容易

拷贝构造函数的引入

首先要清楚堆创建后,除非通过free否则是不会被还原的,因此如果有这样的C语言代码:

void push(Stack ps)
{
  ps._a[0] = 10;
  ps._top++;
}
void test2()
{
  Stack s={0,0,0};
  StackInit(&s);
  push(s);
  printf("%d", s._a[0]);
}

这里的StackInit函数只是单纯的初始化,给栈开辟空间,而最后运行结果是10,原因就在于在push函数中,虽然是值传递,但是ps结构体中的成员_a依旧拥有改变堆内存的能力,具体可以用下面的图来表示

那么现在换到C++,引入类的概念后,整个就变得比C语言要复杂一点,原因如下:

首先,定义一个栈的类,并且完成一系列栈的操作

typedef int STDataType;
class Stack
{
public:
  Stack()
  {
    capacity = 4;
    a = (STDataType*)malloc(sizeof(STDataType) * capacity);
    if (a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    top = 0;
  }
  ~Stack()
  {
    top = capacity = 0;
    free(a);
    a = nullptr;
  }
  void Push(STDataType x)
  {
    if (capacity == top)
    {
      capacity *= 2;
      STDataType* tmp = nullptr;
      tmp = (STDataType*)realloc(a,sizeof(STDataType) * capacity);
      if (tmp == nullptr)
      {
        perror("realloc fail");
        exit(-1);
      }
      a = tmp;
    }
    a[top] = x;
    top++;
  }
  void Pop()
  {
    top--;
  }
  STDataType Top()
  {
    return a[top];
  }
private:
  STDataType* a;
  int top;
  int capacity;
};

和C语言的实现相同,假如我们直接进行传值拷贝,具体做法如下:

void func1(Stack s)
{
   s.Push(1);
}
int main()
{
   Stack s1;
   func1(s1);
   return 0;
}

再画出和上面相仿的图

看似和C语言基本相同,但实际相差很大,C++会执行构造函数和析构函数,那么在进入func1的栈帧后,销毁栈帧的时候就会执行析构函数,_a所指向的空间就被销毁掉了,那么回到main函数的栈帧后,结束程序依旧要进行析构函数,此时_a已经被销毁过一次了,程序就会崩溃,无法正常运行

这其实也就说明,C++中想要直接进行对象拷贝似乎不是一件容易的事,两个对象指向同一片空间就必然会出问题,C++语法就定义了拷贝函数来解决这个问题

拷贝构造函数的特征

拷贝构造函数也是特殊的成员函数,具体表现在:

  1. 拷贝函数是构造函数的一个重载
  2. 拷贝函数的参数只有一个并且必须是类类型对象的引用,使用传值方式编译器会报错,因为涉及到了无穷递归调用
  3. 若未显式定义,编译器会生成默认的拷贝构造函数,默认拷贝构造函数会按对象按内存中的存储字节序完成拷贝,也叫做浅拷贝或值拷贝
  4. 深拷贝就涉及到上面栈在堆上空间的问题
  5. 拷贝构造函数的典型应用场景

使用已存在的对象创建新对象

函数参数类型为类类型对象

函数返回值类型为类类型对象

下面根据拷贝构造函数的特征进行一一分析

1. 拷贝构造函数是构造函数的重载

这个很好解释,拷贝构造函数函数名和构造函数相同,只是函数参数不同

2. 拷贝函数的参数只有一个并且必须是类类型对象的引用,使用传值方式编译器会报错,因为涉及到了无穷递归调用

假设我们这里是这样实现拷贝构造函数:

//函数定义
Date(const Date d1)
{
  _day = d1._day;
  _month = d1._month;
  _year = d1._year;
}
//函数调用
Date d1(d2);

那么标准写法是如何写的呢

//函数定义
Date(const Date& d1)
{
  _day = d1._day;
  _month = d1._month;
  _year = d1._year;
}
//函数调用
void func2(Date d2)
{
  d2.Print();
}
int main()
{
  Date d1(2002, 10, 12);
  d1.Print();
  func2(d1);
  return 0;
}

3. 若未显式定义,编译器会生成默认的拷贝构造函数,默认拷贝构造函数会按对象按内存中的存储字节序完成拷贝,也叫做浅拷贝或值拷贝
4. 深拷贝就涉及到上面栈在堆上空间的问题

这里需要注意的是:

不写拷贝构造函数时,编译默认生成的拷贝构造,和之前的构造函数特性是不一样的

  1. 内置类型是值拷贝
  2. 自定义的类型是调用它的拷贝

简单来说,像Date类型的就不需要我们进行拷贝构造,但是Stack类型的就需要进行深拷贝

下面是深拷贝的拷贝构造函数

Stack(const Stack& s1)
{
  top = s1.top;
  capacity = s1.capacity;
  STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * capacity);
  if (tmp == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  a = tmp;
}

所谓深拷贝,就是重新在堆上开辟一个空间供构造出的栈使用,这样就避免了函数栈帧中的栈在结束时free掉了堆上的空间使得main函数崩溃的情况出现

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

运算符重载

C++在C的基础上的提升在运算符重载上也可以体现出

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

假设我们现在要比较日期谁大,那么这个场景就可以应用运算符重载

// 类体内定义运算符重载
bool operator <(const Date& d1)
{
  if (_year > d1._year)
  {
    return false;
  }
  else if (_year == d1._year && _month > d1._month)
  {
    return false;
  }
  else if (_year == d1._year && _month == d1._month && _day > d1._day)
  {
    return false;
  }
  else
  {
    return true;
  }
}
// 调用main函数
int main()
{
  int i = 10;
  int j = 20;
  int tmp1 = i < j;
  Date d1(2000, 10, 20);
  Date d2(2001, 8, 10);
  int tmp2 = d1 < d2;
  cout << tmp1 << endl;
  cout << tmp2 << endl;
}

我们转到汇编观察

从中也不难发现,运算符重载后调用小于实际上是调用了运算符重载函数

这样写代码的可读性大大提高

赋值运算符重载

关于拷贝构造和赋值运算符重载,你需要知道的…

1. 就运算符重载本身而言,这个函数本身是成员函数

就对上面的代码来说,假设这里执行下面的命令

d1<d2

实际上在施行的时候会转换成这样:

d1.operator<(d2)

再返回对应的值或其他形式

2. 赋值运算符重载和拷贝构造的区别

前面我们讲了拷贝构造要带引用,不带引用会递归

Date d1(const Date d2);            //错误写法
Date d1(const Date& d2);           //正确写法

那么反观运算符重载,表面上看也是这样:

bool operator <(const Date d1)
bool operator <(const Date& d1)

那上面的写法可以吗,其实是可以的,这里就需要对拷贝构造死循环有更深刻的理解

对于拷贝构造来说,它实质上是需要新建对象的,也就是说这里进行拷贝构造函数的时候要先传参再执行函数新建一个对象,而在传参的过程中就会陷入这是否也是拷贝构造的循环中,导致最后陷入死循环导致构造失败

而对于运算符重载来说,这里仅仅只是执行一个类内的成员函数,两个对象都已经创建好了,而这里传参的Date只是把参数传过来而已,如果使用的不是引用,则会创建一个形参,在内存中会有更多的消耗,经此而已,而如果使用引用就避开了创建形参这样的一个过程,相当于是减小了内存的消耗,因此这里并不一定必须传引用,传引用只是提升效率的一种方式

const的含义

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改,后续有具体实际情况再进行分析

取地址及const取地址操作符重载

这是最后一个默认成员函数,但是使用场景极少

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载

相关文章
|
8月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
8月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
8月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
445 6
|
8月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
144 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
226 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
276 12
|
8月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
164 16
|
9月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)