[C转C++之路]再谈构造、隐式转换和静态成员

1,382 阅读7分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

       本文就来分享一波作者对C++的构造函数、隐式转换和静态成员的学习心得与见解。

       笔者水平有限,难免存在纰漏,欢迎指正交流。

再谈构造函数

构造函数赋值

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

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

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

初始化列表

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

比如:

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

       初始化列表和构造函数体内赋初值可以混着来,比如:

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

注意事项

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

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

1.引用成员变量 

2.const成员变量 

3.自定义类型成员(且该类没有默认构造函数时)

       为什么这么说呢?因为它们必须在定义的时候初始化,然而仅凭构造函数是无法实现的,所以就用初始化列表来实现,也就是说对象的每个成员是在初始化列表处定义并初始化的(对象整体是在类实例化的时候定义的)。

       没有默认构造函数说明构造函数需要传参,这时就必须通过初始化列表来初始化了。

例子:

 class 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成员变量
 };

3. 一个类尽量使用初始化列表初始化,因为不管你是否使用初始化列表,成员变量都会先使用初始化列表初始化,对于一般的内置类型则可以在构造函数体内赋初值。

       如果在初始化列表中显式写了的就用初始化列表的来初始化。

       如果没有在初始化列表中显式初始化的话:

       对于内置类型,构造函数体内可以赋初值,都不行的话会用缺省值,没有就用随机值。

       对于自定义类型,会调用它的默认构造函数,如果没有默认构造函数的话就报错。

       看这样一个问题:任何类都需要有默认构造函数,否则会报错。

       对吗?不对,这才刚讲了初始化列表嘛,你看上一个代码示例,如果类里面没有默认构造函数,可以用初始化列表来初始化,这样就不会报错。

       有些情况下会报错,比如

 class A
 {
 public:
     A(int a)
     {}
 private:
     int _a1;
 }
 ​
 class B
 {
 public:
     B()//会自动调用A的默认构造函数,但是A没有,所以会报错
     {}
 private:
     A _aa;
 }

       一个类尽量提供默认构造函数,而且推荐提供全缺省参数(既带参又属于默认构造),因为你写的这个类有可能会成为其他类的成员,有可能其他类的构造函数会调用你这个类的默认构造函数,而如果你没有在类里面提供的话就会报错。

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

例子:

       打印结果会是什么呢?

 class A
 {
 public:
     A(int a)
         :_a1(a)
         ,_a2(_a1)
     {}
     void Print() 
     {
         cout<<_a1<<" "<<_a2<<endl;
     }
 private:
     int _a2;
     int _a1;
 };
 ​
 void testA() 
 {
     A aa(1);
     aa.Print();
 }

image-20221011170439371

       为什么会这样呢?成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关 。所以说这里是_a2先初始化,用的是_a1的值,而此时_a1还没初始化是随机值,_a2就初始化为了随机值,而_a1就用传入的值1初始化了。

类型转换与explicit关键字

隐式转换

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

先来看看这个例子:

 class Date
 {
 public:
     Date(int year)
         :_year(year)
     {}
 private:
     int _year;
     int _month;
     int _day;
 };
 ​
 void Test()
 {
     Date d1(2022);
     Date d2 = 2022;//C++98支持这一行为
 }

       是不是觉得很奇怪?你这一个Date类另一个整型的怎么能够赋值呢?你这里的赋值重载是默认的,两个操作数都是Date类对象啊,就算你说这里发生类型转换也让人奇怪——整型变量怎么能转成Date类对象呢?

       少安毋躁,这里确实发生了隐式类型转换,且听我细细道来。

       看一下这段代码:const Date& d3 = 2022;,有没有印象?这个在之前的文章里提到过。

       关于隐式类型转换,在讲引用那里提到过,如下图:

image-20221013090453802

       C++98对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数支持用一个与参数同类型的值来进行隐式类型转换,听起来有点不好理解,我们直接上例子:

image-20221013092519380

       如果是多参数构造函数支持类型转换吗?

       C++11支持了,比如:Date d4 = {2022, 10, 12};const Date& d5 = {2022, 10 ,12};,原理和前面单参构造函数是一样的,相当于对以前的标准进行了一些拓展。

explicit关键字

       单参构造函数,没有使用explicit修饰,具有类型转换作用。

       对于下面这个构造函数,虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用。如果下面这个改成全缺省也是可以发生隐式类型转换,因为全缺省很灵活,可以看成无参、单参或多参。

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

       而使用explicit修饰构造函数,禁止类型转换,此时如果还是想要转换就会报错。

 explicit Date(int year, int month = 1, int day = 1)
     : _year(year)
     , _month(month)
     , _day(day)
     {}

image-20221013093406124

       这个报错现在不明白也没关系,后面会再提。

static成员

概念

       声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

       由一个面试题入手:实现一个类,计算程序中创建出了多少个类对象。

       你可能会想到,创建对象的时候会调用构造函数或默认构造函数,那只要每构造一次我就让计数器+1不就好了吗,欸,要注意一下临时对象在当前语句执行完后就会析构,所以每析构一次让计数器-1。

 class A
 {
 public:
     A() { ++scount; }
     A(const A& t) { ++scount; }
     ~A() { --scount; }
     
 private:
     int _a;
 };

       那么问题来了,这个计数器放哪呢?

       直接作为类的成员变量吗?不行,这样的话每个对象都有一个该变量并且相互独立,根本无法计数。

       那作为全局变量,这样不就不受对象干扰了嘛。可是你有没有考虑过安全性?C++中不提倡使用全局变量,因为安全性较差容易被修改(基本上大家都能改,因为没有限制)。所以不应该使用全局变量。

       我们不妨试一试静态成员:

 class A
 {
 public:
     A() { ++_scount; }
     A(const A& t) { ++_scount; }
     ~A() { --_scount; }
     static int GetACount() { return _scount; }//使用静态成员函数才能够访问静态成员变量
 private:
     static int _scount;//仅仅是声明
 };
 ​
 int A::_scount = 0;//定义和初始化
 void TestA()
 {
     cout << A::GetACount() << endl;
     A a1, a2;
     A a3(a1);
     cout << A::GetACount() << endl;
 }

       可以结合下面的特性总结来看待这份代码。

特性

  1. 静态成员变量为所有类对象所共享,受static修饰而存放在静态区,不属于某个具体的对象

  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

  3. 类静态成员受类作用域限制,可用 类名::静态成员 或者 对象.静态成员 来访问

    所以静态成员函数不需要对象即可使用:A::GetACount()

  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员变量而只能访问静态成员变量

  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

【问题】

1. 静态成员函数可以调用非静态成员函数吗?

       我们对前面的A类进行一些修改:

     class A
     {
     public:
         A() {++_scount; }  
         A(const A& t) { ++_scount; }
         ~A() { --_scount; }
         
         int Add(int b)
         {
             ++b;
             return b;
         }
         
         static int GetACount() 
         { 
             _scount = Add(_scount);//不可行
             return _scount; 
         }
     private:
         static int _scount;
     };

       我们发现静态成员函数好像是不可以调用非静态成员函数。

image-20221013103032535

       一般的非静态成员函数无法调用,但有个例外,静态成员函数可以调用构造函数。

2. 非静态成员函数可以调用类的静态成员函数吗?

       我们对前面的A类进行一些修改:

 class A
 {
 public:
     A() 
     {
         int cnt = GetACount();
         ++_scount; 
     }
     A(const A& t) { ++_scount; }
     ~A() { --_scount; }
     int Add(int b)
     {
         ++b;
         return b;
     }
     static int GetACount() {return _scount; }
 private:
     static int _scount;
 };

       编译运行后发现没问题,所以非静态成员函数可以调用类的静态成员函数。

image-20221013103343846

实例讲解

       题目链接:求1+2+3+...+n牛客题霸牛客网 (nowcoder.com)

image-20221013103532959

题目提供的初始代码如下:

 class Solution {
 public:
     int Sum_Solution(int n) {
         
     }
 };

       之所以讲这道题是因为它很适合用我们刚讲的静态成员来解决。

       我们定义一个n个元素的对象数组(这里先用C的变长数组),定义两个静态成员变量,每次调用构造函数就让一个变量来迭代,另一个变量来累加,这样一来不就实现了从1加到n了嘛。

 class Sum
 {
 public:
     Sum()
     {_sum = ++_cnt + _sum; }
     static int GetSum()
     { return _sum;}
 private:
     static int _cnt;
     static int _sum;    
 };
 ​
 int Sum::_cnt = 0;
 int Sum::_sum = 0;
 ​
 class Solution 
 {
 public:
     int Sum_Solution(int n) 
     {
         Sum sum[n];
         return Sum::GetSum();
     }
 };
 ​

骚操作:要求类对象只能在创建在栈上

       我们知道,类对象可以存储在栈区、堆区乃至静态区,那如果我就必须限制类对象只能创建在栈上该怎么办呢?

       下面是三种创建对象的方法:

 class A
 {
 public:
     A(int a = 0)
         :_a(a)
     {}
 private:
     int _a;
 };
 ​
 void test
 {
     static A a1;
     A* pa2 = new A;
     A a3;
 }

       发现它们都需要通过构造函数来创建对象,那我们不妨“把门堵死”——构造函数私有化,这样上述三种方法都不能创建对象了。

       如此一来又如何只把对象创建在栈上呢?

       你可能会想到写一个公有函数去调用构造函数并返回构造的对象,这看起来好像可以,可是有一个大问题,你这函数是用来创建对象的,但是调用你这个函数需要指定对象来调用,那对象不还没创建嘛...这就陷入了“先有鸡还是先有蛋”的问题中去了。

 class A
 {
 public:
     A GetObj(int a = 0)
     {
         A aa(a);
         return aa;
     }
 private:
     A(int a = 0)
         :_a(a)
     {}
 private:
     int _a;
 };
 ​
 void test
 {
     //static A a1;
     //A* pa2 = new A;
     //A a3;
     A a4 = GetObj();
 }

       那怎么办?这时候就可以把GetObj函数写成静态的了,这样一来就可以直接通过类名指定来使用该函数。

 class A
 {
 public:
     static A GetObj(int a = 0)
     {
         A aa(a);
         return aa;
     }
 private:
     A(int a = 0)
         :_a(a)
     {}
 private:
     int _a;
 };
 ​
 void test
 {
     //static A a1;
     //A* pa2 = new A;
     //A a3;
     A a4 = A::GetObj();
 }

以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif