C++入门4——类与对象3-1(构造函数的类型转换和友元详解)

简介: C++入门4——类与对象3-1(构造函数的类型转换和友元详解)

1. 再识构造函数

1.1 构造函数体赋值

C++入门3——类与对象(2)中,我们已经知道了构造函数的基本功能是给对象中的各个成员变量赋一个合适的初始值,这个初始化的过程是在构造函数体内部进行的:

class Date
{
public:
  Date(int year, int month, int day)
  {
        //构造函数体内部初始化
    _year = year;
    _month = month;
    _day = day;
  }
 
private:
    //成员变量的声明
  int _year;
  int _month;
  int _day;
};
int main()
{
  //调用Date构造函数
  Date d1(2002, 7, 7);
  return 0;
}

需要再度明确的是:

private下的int _year;int  _month;int _day;只是对成员变量的声明,并没有开创空间,所以这些成员变量并不是在此刻定义的,既然这样,那么他们又是在何处定义的呢?

事实上,在上面代码中,成员变量在构造函数体内部的初始化其实就是定义成员变量的一种方法。


之前的成员变量都是普通类型,C++已经学到现在了,我们总归是要尝试新类型,活出不一样的人生。

现在,我要用到引用&类型和const类型,还用之前的方法定义Date构造函数:

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

我们会发现报错了:

这是为什么呢?

我们需要有清晰认知:

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

C++入门1——从C语言到C++的过渡中,我们知道,引用&和const在定义时就要进行初始化,可是这样来定义构造函数编译器又会报错,如何来解决呢?这时就要用初始化列表来解决了。

1.2 初始化列表

初始化列表:以冒号开始,以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。这便是定义成员变量的另一种方法。

class Date
{
public:
  Date(int year, int month, int day)
        //初始化列表初始化
    :_year(year)
    ,_month(month)
    ,_day(day)
 
    ,_ret(year)
    ,_x(1)
  {}
 
private:
  int _year;
  int _month;
  int _day;
 
  int& _ret; // 引用 : 必须在定义的时候初始化
  const int _x; // const : 必须在定义的时候初始化
};

也可以混着用:

class Date
{
public:
  Date(int year, int month, int day)
        //初始化列表
    : _ret(year)
    , _x(1)
  {
        // 剩下3个成员没有在初始化列表显示写出来定义
        // 但是他也会定义,只是内置类型默认给的随机值
        // 如果是自定义类型成员会去调用它的默认构造函数
  
        // 函数体内部初始化
      _year = year;
    _month = month;
    _day = day;
  }
 
private:
  int _year;
  int _month;
  int _day;
 
  int& _ret;
  const int _x;
};

注意:

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

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

引用成员变量;

const成员变量;

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

&引用成员变量和const成员变量已经说过,接下来着重探讨一下自定义类型成员变量:

我们知道,类都会默认生成6个默认成员函数(请看类的6个默认成员函数 ) ,构造函数就是其中一个,在我们不显示定义构造函数时,编译器会生成一个默认的构造函数供我们使用,并且是自动定义自动调用,这为我们提供了许多方便。

可是一些类,如顺序表、链表、栈和队列等数据结构类,它们的默认构造函数往往并不符合我们的要求,我们当然需要自己定义构造函数,如下:

#include <iostream>
using namespace std;
 
typedef int DataType;
 
class SeqList//SeqList类
{
public:
  SeqList()
  {
    _a = (DataType*)malloc(sizeof(DataType) * 4);
    if (_a == nullptr)
    {
      perror("malloc failed");//如果扩容失败,说明原因
      exit(-1);//直接退出
    }
    _size = 0;//当size≥capacity时就动态开辟空间
    _capacity = 4;//初始化数组容量为4
  }
 
  //打印
  void Print()
  {
    for (int i = 0; i < _size; i++)
    {
      cout << _a[i] << endl;
    }
  }
 
private:
  int* _a;
  int _size;
  int _capacity;
};
 
class MySL
{
public:
  MySL()
    :_sl()
  {}
 
private:
  SeqList _sl;//自定义类型成员,且该类没有默认构造函数
};

所以,自定义类型成员(且该类没有默认构造函数)在初始化时也需要用初始化列表初始化。


思考如下代码的运行结果:

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

运行结果:

首先把结论抛出来:

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

成员变量声明时,先声明的是a2,其次是a1,所以在初始化时,先初始化a2,再初始化a1

在调用函数时把a传递给了a1, 而在传值时,先把a1传递给a2,此时a1是随机值,故a2也是随机值,然后再把a传递给a1,a1就是1。


总结:

初始化列表解决的问题:

1. 必须在定义的地方显示初始化 ①引用 ②const ③没有默认构造自定义成员;

2. 有些自定义成员想要显示初始化,自己控制;

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

4. 刚说完尽量使用初始化列表初始化,那么以后我们都用初始化列表,摒弃函数体内部初始化可以吗?

答案是不能,因为有些初始化或者检查的工作,初始化列表也不能全部搞定,就像上面的SeqList类的检查扩容,初始化列表就不能完成工作。

因此我们对初始化列表应该抱有能用尽用的原则,对于实在不能用初始化列表的,应该使用函数体内部初识化。

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


1.3 构造函数的类型转换与explicit关键字

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

class Date
{
public:
  Date(int year)
    :_year(year)
  {}
 
  void Print()
  {
    cout << _year << endl;
  }
 
private:
  int _year;
};

函数调用时:

int main()
{
  Date d1(2002);
  Date d2 = 2003;//发生了隐式类型转换,内置类型int转换为自定义类型
  d1.Print();
  d2.Print();
  return 0;
}

代码正常编译通过,那么Date d2=2003;为什么能够编译通过呢?

因为这里发生了隐式类型转换,能支持这里的类型转换是因为Date构造函数的单参数类型为int(如果是多参数构造函数,第一个参数未缺省的半缺省函数或全缺省函数也支持隐式类型转换)。

也就是说,这里先产生了一个临时变量 tmp,tmp调用构造函数将其初始化为2003,再调用拷贝构造将tmp拷贝给d2。


为了更加了解此过程的临时变量,我们先拿已经很熟悉的int类型转double类型举例:

    int a = 3;
  double b = a;

我们知道,&引用是对相同类型变量起别名,现在,既然int与double能够相互转换,我就用double类型对int类型的a取别名:

    int a = 3;
  double b = a;
  double& b1 = a;

这里为什么又不行了呢?

其实这里报错并不是因为类型不同,是因为b1并不是对a取别名, 而是对产生的临时变量取别名,临时变量具有常属性,所以需要在前面加一个const:

    int a = 3;
  double b = a;
  const double& b1 = a;

既然存在隐式类型转换,那么我们有没有方法阻止隐式类型转换呢?

这时explicit关键字的作用就来了:如果我们不想发生隐式类型转换,就在构造函数的前面加explicit:

class Date
{
public:
  explicit Date(int year)
    :_year(year)
  {}
 
  void Print()
  {
    cout << _year << endl;
  }
private:
  int _year;
};


思考如下代码的运行结果:

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

答案是否出乎你的预料呢?

这是因为这里的Date d2 = (2003, 7, 7);括号里其实是逗号表达式(如果忘记了逗号表达式,可以看初始C语言5——操作符详解),等价于Date d2=7;


2. static成员

2.1 static成员的引入

了解static成员之前,先做一道题:

实现一个类,计算程序中创建出了多少个类对象。

解析:首先应该明确都在什么情况下会创建类对象:调用构造函数时会创建类对象,如果程序中遇到传值,会调用拷贝构造函数,这时也会创建类对象,所以如果设计数器,在构造函数和拷贝构造函数里面都需要计数。

解法1:使用全局的count计数器

#include <iostream>
using namespace std;
namespace xxk
{
  int count = 0;//count与C++库函数重名,避免重名,放到命名空间内
}
class A
{
public:
  A() { ++xxk::count; }
  A(const A& t) { ++xxk::count; }
  ~A() {}
private:
};
A func()
{
  A a;
  return a;
}
int main()
{
  A a1;
    func();
  cout << xxk::count << endl;
  return 0;
}

解法1可行,可是要知道,我现在计算的是A对象,如果程序里还有一个B对象,那么最后得到的count值就是A和B两个对象的类对象数量了呀!所以把count设为全局变量并不是很靠谱。


解法2:使用成员变量的count计数器

#include <iostream>
using namespace std;
class A
{
public:
  A() { ++count; }
  A(const A& t) { ++count; }
  ~A() {}
private:
  int count=0;
};
A func()
{
  A a;
  return a;
}
int main()
{
  A a1;
  func();
  cout << count << endl;
  return 0;
}

结果显示这样的程序根本跑不动:成员变量count只在本作用域起作用,也就是说,A类的每个对象都会有一个count,a1对象的count只属于a1,a2对象的count只属于a2......

解法1存在弊端,解法2又是bug,那现在问题的最优解是什么呢?

2.2 static成员的用法及特性

这时就要用到static成员了:用static修饰的成员变量属于这个类的全部对象

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

最优解:

#include <iostream>
using namespace std;
class A
{
public:
  A() { ++count; }
  A(const A& t) { ++count; }
  ~A() {}
//private:
  static int count ;
};
int A::count = 0;
A func()
{
  A a;
  return a;
}
int main()
{
  A a1;
  func();
  cout << A::count << endl;
  return 0;
}

上面的成员变量count设为公有,因为类外无法访问类的私有成员变量。

如若想将其设为私有又想正常访问,可以借鉴日期类获取大小月,润平月的接口函数一样,定义一个静态成员函数:

故真正的最优解为:

#include <iostream>
using namespace std;
class A
{
public:
  A() { ++count; }
  A(const A& t) { ++count; }
  ~A() {}
  //设置为静态成员函数,没有了this指针
  //只能在类外调用,不会传参,更不会修改count
   static int Acount()
  {
    return count;
  }
private:
  static int count;
};
int A::count = 0;
A func()
{
  A a;
  return a;
}
int main()
{
  A a1;
  func();
  cout << a1.Acount()<< endl;
  //如果未定义a1,为了调用Acount函数,不得不定义一个A变量
  A a;
  cout << a.Acount()-1 << endl;//为调用而定义,所以需要-1
  return 0;
}


总结:

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;

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

3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问;

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

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

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