【C++杂货铺】再谈类和对象(一)

简介: 【C++杂货铺】再谈类和对象(一)

eaf3f17dc12b44baaebc9776d9552432.gif

一、再谈构造函数

1.1 构造函数体赋值

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

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

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

1.2 初始化列表

📖定义:

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

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

小Tips: 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)。出了下面提到的三个类型的成员变量外,其他的成员变量可以不出现在初始化列表中,此时编译器对内置类型(没有默认值的情况下)不做处理(一般是随机值),对自定义类型会调用它的默认构造,内置类型如果给了默认值,则编译器会使用这个默认值。

📖必须经过初始化列表

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

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

其中引用成员变量和const成员变量,都有一个共同的特征:必须在定义的时候初始化。初始化列表就是对象中成员变量定义的位置。

class A
{//A类中没有默认构造函数
public:
  A(int a)
    :_a(a)
  {}
private:
  int _a;
};
class B
{
public:
  B(int a, int& ref)
    :_aobj(a)
    ,_ref(ref) 
    ,_n(10)
  {}
private:
  A _aobj;  // 没有默认构造函数
  int& _ref;  // 引用
  const int _n; // const 
};

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

📖初始化列表能代替函数体内赋值嘛?

class Stack
{
public:
  Stack(int capacity = 10)
    :_top(0)
    , _capacity(capacity)
    , _a((int*)malloc(_capacity*sizeof(int)))
  {
    //下面这些功能都是初始化列标无法完成的,因此需要用到构造函数的函数体
    if (_a == nullptr)`在这里插入代码片`
    {
      perror("malloc fail");
      exit(-1);
    }
    cout << 11111111111111111111 << endl;
    memset(_a, 0, _capacity * sizeof(int));//把空间中的数据全部设置为0
  }
private:
  int* _a;
  int _top;
  int _capacity;
};

如上面的代码所示,初始化列表并不能完成所有工作,有时候对于成员变量不仅要完成初始化,还要对初始化的结果进行合理性检查等操作,因此初始化列表不能代替函数体内赋值。

📖初始化顺序

成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

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();
}

9cb25e77f7b943a5a14fa54d2253d980.png

上面代码中,因为A类中成员变量的声明顺序是_a2、_a1,所以在初始化列表中先去初始化_a2,但是_a2是用_a1来初始化的,_a1此时还没有被初始化,所以是随机值,接下来再去用a初始化_a1,所以最终打印出来的结果_a1是1,而_a2是随机值。

1.3 explicit关键字

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

class A
{
public:
  A(int x)
    :_a(x)
  {
    cout << "A(int x)" << endl;
  }
  A(const A& x)
    :_a(x._a)
  {
    cout << "A(const A& x)" << endl;
  }
private:
  int _a;
};
int main()
{
  A a2 = 2;
  return 0;
}

上面代码中,A类只有一个单参数的构造函数,因此该构造函数是支持隐式类型转换的,A a2 = 2;本质上就是隐式类型转换,把一个整型2,转换成自定义类型A。具体过程是:首先在隐式转换过程中会产生一个临时的中间变量,这里就是用2去调用构造函数,得到一个A类型的临时中间变量,然后再用这个A类型的中间变量去调用拷贝构造,最终完成a2的创建。一般比较新的编译器,对这种连续的调用构造、拷贝构造进行了优化,会用2去调用构造函数完成a2的创建。

70be6634028a4625beda16276793d262.png

打印的结果确实说明只调用了构造函数。此时可能会有朋友产生怀疑了,觉得A a2 = 2;本来就是直接用2去调用构造函数创建a2,压根不存在什么先创建临时的中间变量,别急,可以通过引用来验证。

25098b15fefa457587989a63798b89e3.png

e67b0a247466460794da416ec97924af.png

这里我们把一个整型3,赋值给一个A类型的引用,起初我们没加const程序报错了,后面加上const程序没有报错。为什么?就是因为这里会首先用3去调用构造函数,创建一个A类型的临时中间变量,前面的文章说过,临时的中间变量具有常性,这里的a3就是这个临时中间变量的别名,所以要在a3的前面加上const进行修饰。

📖使用场景

//string是字符串类
string name1("张三");
//直接构造
string name2 = "张三";
//构造+拷贝构造,优化成构造
class list
{
public:
  void push_back(const string& str)
  {}
};
int main()
{
  list l1;
  string name2("李四");
  l1.push_back(name2);
  l1.push_back("李四");
  return 0;
}

如上面的代码,我们在插入值的时候,因为push_back函数的参数是string类型的对象引用,意味着要插入一个string类型的对象,如果不支持隐式类型转化,在插入string对象的过程中,我们就要先创建一个string类型的对象,然后再去插入,支持隐式类型转换的话,我们就无需创建string类型的对象,而是直接把一个字符串插入,就像l1.push_back("李四");这样,先用"李四"创建一个临时的中间变量,临时中间变量具有常性。此时就体现出了,在不修改对象的情况下,给形参加上const的优越性。push_back函数的形参用引用,是为了避免调用拷贝构造,一旦碰到形参是引用的,就要仔细考虑要不要加const进行修饰,引用和权限问题永远是并存的。

📖explicit关键字

如果想要禁止上面提到的隐式类型转换,可以在构造函数的前面加上explicit关键字进行修饰。

class A
{
public:
  explicit A(int x)
    :_a(x)
  {
    cout << "A(int x)" << endl;
  }
private:
  int _a;
};

58b8de26cde04f0ca7680e10381323c2.png

此时就不能把一个整型赋值给A类型的对象。智能指针就不希望发生这种隐式类型转换,具体的我们后面再说。

二、static成员

📖先看一个场景

有一个A类,现在要统计程序中正在使用的A类型的对象有多少个,即当前程序中,创建后没有被销毁的A对象的个数。

很多朋友第一时间想到的就是定义一个全局的整型变量并初始化为0,然后在构造函数中++,在析构函数中--,像下面这样:

int _scount = 0;//全局的变量用来统计个数
class A
{
public:
  A()
  { 
    cout << "A()" << endl;
    ++_scount; 
  }
  A(const A& t)
  {
    cout << "A(const A& t)" << endl;
    ++_scount; 
  }
  ~A() 
  {
    cout << "~A()" << endl;
    --_scount; 
  }
public:
  int _a = 10;
};
A a1;//第一个,调用的普通构造
A Func(A aa)//形参是类对象,则需要调用拷贝构造//第四个
{
  cout << __LINE__ << ":" << _scount << endl;
  return aa;
}
int main()
{
  cout << __LINE__ << ":" << _scount << endl;
  A a2;//第二个,调用的普通构造
  static A a3;//第三个,调用的普通构造
  Func(a3);
  //函数传值返回会创建一个临时的中间变量,也是调用拷贝构造,用现有的aa对象去创建一个新的对象,所以是拷贝构造//第五个
  cout << __LINE__ << ":" << _scount << endl;
  return 0;
}

1ea04bb946094875abea4a503260b08c.png

注意:全局对象先于局部对象进行构造,局部对象按照出现的顺序进行构造,无论是否为static,析构的顺序是按照析构造的相反顺序析构,只需注意static会延长对象的生命周期,所以会放在局部对象之后进行析构。

上面这种方法可以帮我们统计出当前程序中“存活”的A类对象,但是也有一个缺陷,这里的计数器变量_scount 是一个全局的,意味着我们可以在程序中的任何地方对它进行修改,这样就会导致我们统计出来的数量不准确。

为了解决上面的问题,我们可以考虑用C++的封装性,即把这个计数器变量_scount 变成A类的静态成员变量。

class A
{
public:
  A()
  {
    cout << "A()" << endl;
    ++_scount;
  }
  A(const A& t)
  {
    cout << "A(const A& t)" << endl;
    ++_scount;
  }
  ~A()
  {
    cout << "~A()" << endl;
    --_scount;
  }
public:
  int _a = 10;
  static int _scount;
};


目录
相关文章
|
6天前
|
C++ 容器
C++中自定义结构体或类作为关联容器的键
C++中自定义结构体或类作为关联容器的键
13 0
|
6天前
|
存储 安全 编译器
【C++】类和对象(下)
【C++】类和对象(下)
【C++】类和对象(下)
|
4天前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
4天前
|
编译器 C++
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
|
4天前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
6天前
|
编译器 C++
【C++】类和对象(中)
【C++】类和对象(中)
|
6天前
|
存储 编译器 程序员
【C++】类和对象(上)
【C++】类和对象(上)
|
6天前
|
存储 编译器 C++
【C++】类和对象(下)
【C++】类和对象(下)
|
6天前
|
存储 算法 搜索推荐
【C++】类的默认成员函数
【C++】类的默认成员函数
|
11天前
|
C++
C++ --> 类和对象(三)
C++ --> 类和对象(三)
26 9