【C++】类和对象(下)(1)

简介: 【C++】类和对象(下)(1)

👉再谈构造函数👈


构造函数体赋值


在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。


class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};


虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。


初始化列表


初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式


class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};


现在我们大概知道了初始化列表的大致玩法,那我们把之间写的栈Stack也用初始化列表来初始化一下。


class Stack
{
public:
  Stack(int capacity = 4)
    : _top(0)
    , _capacity(capacity)
    , _a((int*)malloc(sizeof(int) * capacity))
  {
    if (_a == NULL)
    {
      perror("malloc fail");
      exit(-1);
    }
  }
private:
  int* _a;
  int _top;
  int _capacity;
};


栈Stack的初始化列表除了可以像上面那样写,初始化列表还可以和函数体内赋初值一起使用,见下方代码:


#include <iostream>
using namespace std;
class Stack
{
public:
  Stack(int capacity = 4)
    : _top(0)
    , _capacity(capacity)
  {
    _a = (int*)malloc(sizeof(int) * capacity);
    if (_a == NULL)
    {
      perror("malloc fail");
      exit(-1);
    }
  }
private:
  int* _a;
  int _top;
  int _capacity;
};


因为初始化列表有些事情做不了,所以可以在构造函数内做。比如:给申请的空间全部赋上初值 0 memset(_a, 0, sizeof(int) * capacity)。


注意:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。


以上的例子似乎都无法解释初始化列表为什么要存在,因为似乎初始化列表做的时期,在函数里面也能做。


那什么样的场景一定需要初始化列表,而函数体内赋初值无法完成呢?我们一起来看一下:


21e293494181468bbd026f07b34fcec6.png



当类B的成员变量用const修饰时,如果在函数体内给const修饰的成员变量赋初值,这时候就会出现语法错误。这就相当于修改一个const修饰的只读变量。const关键字修饰的变量在定义的时候必须初始化,而且只能初始化一次。所以这时候就需要借助初始化列表来完成这个工作了。因为对象的实例化是调用构造函数来整体定义的,而对象中的成员变量是在初始化列表中定义初始化的。


#include <iostream>
using namespace std;
class B
{
public:
  B()
    :_n(10)
  {}
private:
  const int _n; // const
};
int main()
{
  B b;
  return 0;
}


注意:对象的每个成员变量都需要走初始化列表,就算显式在初始化列表写,也会走。 那么如果不在初始化列表中初始化类B的_n,就会报错。因为初始化列表是每个成员变量定义的地方,而const修饰的变量在定义的时候必须初始化且只能初始化一次。


e48f8cec024e4a4b80ef0a57c9817503.png


如果你没有显式写初始化列表,也可以用缺省值的方式来解决。因为没有显式写的时候,初始化列表就会用上这个缺省值;如果写了,初始化列表就不会用这个缺省值。


class B
{
public:
  B()
  {}
private:
  const int _n = 1; // const
};
int main()
{
  B b;
  return 0;
}


注:如果没有在初始化列表显式初始化,对于内置类型,有缺省值就用缺省值,没有缺省值就用随机值;而对于自定义类型,就会去调用该自定义类型的默认构造函数。如果没有默认构造函数,就会报错。

0da56b5c7bb94649a8a3fe619667a833.png


可以看到,上面的例子就很好地验证了上面的结论。那我们只需要提高类A的默认构造函数就可以解决上面的问题了。


267777ab3cdc46578bd163721f2b9e2c.png


3ceba48581bf4493b3347b7af949a99f.png

b7a16664adf34dbead2ac4b5bee7efcf.png


如果显示写了初始化列表,就会用初始化列表的初始化。


class A
{
public:
  A(int a = 1)
    :_a(a)
  {}
private:
  int _a;
};
class B
{
public:
  B()
    : _n(10)
    , _aa(100)
  {}
private:
  const int _n = 1; // const
  A  _aa;
};

那么,我们来看一下队列MyQueue的初始化列表。


#include <iostream>
using namespace std;
class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int) * capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  // st2(st1)
  Stack(const Stack& st)
  {
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._top);
    _top = st._top;
    _capacity = st._capacity;
  }
  ~Stack()
  {
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
  void Push(int x)
  {
    // 扩容
    _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
class MyQueue {
public:
  MyQueue()
  {}
  void Push(int x)
  {
    _pushST.Push(x);
  }
private:
  Stack _pushST;
  Stack _popST;
  int _size;
};
int main()
{
  MyQueue q;
  return 0;
}

fc15844e2b9343ea838d1289d1996c2a.png


可以看到,队列MyQueue的初始化列表什么都不写也可以,因为对于自定义类型,会调用该自定义类型的默认构造函数。如果没有提供栈Stack的默认构造函数或者对队列MyQueue的初始空间有要求,就需要写初始化列表了。


#include <iostream>
using namespace std;
class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int) * capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  // st2(st1)
  Stack(const Stack& st)
  {
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._top);
    _top = st._top;
    _capacity = st._capacity;
  }
  ~Stack()
  {
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
  void Push(int x)
  {
    // 扩容
    _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
class MyQueue {
public:
  MyQueue(int capacity)
    : _pushST(capacity)
    , _popST(capacity)
  {}
  void Push(int x)
  {
    _pushST.Push(x);
  }
private:
  Stack _pushST;
  Stack _popST;
  int _size;
};
int main()
{
  MyQueue q(100);
  return 0;
}


可以看到,队列MyQueue的初始化列表什么都不写也可以,因为对于自定义类型,会调用该自定义类型的默认构造函数。如果没有提供栈Stack的默认构造函数或者对队列MyQueue的初始空间有要求,就需要写初始化列表了。


#include <iostream>
using namespace std;
class Stack
{
public:
  Stack(int capacity = 4)
  {
    _a = (int*)malloc(sizeof(int) * capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    _top = 0;
    _capacity = capacity;
  }
  // st2(st1)
  Stack(const Stack& st)
  {
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._top);
    _top = st._top;
    _capacity = st._capacity;
  }
  ~Stack()
  {
    cout << "~Stack()" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
  void Push(int x)
  {
    // 扩容
    _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
class MyQueue {
public:
  MyQueue(int capacity)
    : _pushST(capacity)
    , _popST(capacity)
  {}
  void Push(int x)
  {
    _pushST.Push(x);
  }
private:
  Stack _pushST;
  Stack _popST;
  int _size;
};
int main()
{
  MyQueue q(100);
  return 0;
}


这就是初始化列表的基本内容了。那我们再来想一个问题:什么样的成员变量一定需要初始化列表来初始化?是不是引用一定要在初始化列表中初始化,因为引用只有一次初始化的机会且也只能初始化一次。


结论:

类中包含以下成员,必须放在初始化列表位置进行初始化

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)


注意:尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化。


接下来我们来看一道题目:


#include <iostream>
using namespace std;
class A
{
public:
  A(int a)
    :_a1(a)
    , _a2(_a1)
  {}
  void Print() {
    cout << _a1 << " " << _a2 << endl;
  }
private:
  int _a2;
  int _a1;
};
int main() 
{
  A aa(1);
  aa.Print();
} 
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值


可能很多人的答案都是 A,但是正确答案是 D。


b188608b682d43868a5fc5a4fd9a4e7f.png


那为什么是这样呢?是因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。


从这道题里可以看出,选择题是非常坑的。做选择题一定要小心,不小心一点就会掉进出题人埋的坑了。注:说得比较绝对的选项大概率是错的。 比如:一个类必须提供默认构造函数,不提供就会报错。这句话就是错的,只要我们不调用就不会报错,调用了就会报错。


#include <iostream>
using namespace std;
class A
{
public:
  A(int a)
    :_a1(a)
    , _a2(_a1)
  {}
  void Print() {
    cout << _a1 << " " << _a2 << endl;
  }
private:
  int _a2;
  int _a1;
};
int main() 
{
  return 0;
}

67d5a9424b1649be978ab8f603ec20c1.png

explicit 关键字


构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有隐式类型转换的作用。


所谓的隐式类型转换,就是用一个类型的数据给不同类型的数据进行赋值时产生的转换。进行隐式转换时,会产生临时变量,临时变量具有常属性。因为临时变量具有常属性,所以使用引用时需要加上const修饰。

887d3265b62e4c02b73346e373977e59.png


那自定义类型呢,也支持隐式类型转换,但要求其构造函数只有单个参数或者除第一个参数无默认值其余均有默认值。


单参数构造函数隐式类型转换(C++98)


#include <iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2022);
  //隐式类型转换
  Date d2 = 2022;
  const Date& d3 = 2022;
  return 0;
}


22bb1a0bc4134f3c992092ba1dcc663f.png


explicit 修饰构造函数禁止隐式类型转换


如果我不想让自定义类型的隐式转换发生,我们就可以在构造函数前加上explicit关键字。


e88fac9e000848ed9daaab7edee5b0b9.png


那这种隐式类型转换有什么用呢?有了自定义类型的隐式类型转换就会非常地方便,见下方代码:

#include <iostream>
using namespace std;
#include <string>
void push_back(const string& str)
{
  //...
}
int main()
{
  string s1("hello");
  push_back(s1);
  string s2 = "hello";
  push_back(s2);
  push_back("hello");
  return 0;
}


在编译器的优化过后,它们都只需要调用构造函数就行了,且用起来很方便。所以支持自定义类型的隐式类型转换还是非常必要的。


多参数构造函数隐式类型转换(C++11)


#include <iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1 = { 2022, 11, 13 };
  // 等价于
  Date d2(2022, 11, 13);
  const Date& d3 = { 2022, 11, 13 };
  return 0;
}






















相关文章
|
15天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
21天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
54 5
|
27天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
56 4
|
28天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
66 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
23 1
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
17 0
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)