【C++初阶:类和对象(下篇)】初始化列表 | static成员 | 友元 上

简介: 【C++初阶:类和对象(下篇)】初始化列表 | static成员 | 友元

文章目录

【写在前面】

这篇文章是对类和对象的一个收尾和补充

一、再谈构造函数

💦 构造函数体赋值

❓ 引出初始化列表 ❔

class A
{
public:
  A(int a = 0)
  {
    _a = a; 
  }
private:
  int _a;
};
class B
{
private:
  int _b = 1;
  A _aa;
};
int main()
{
  B b;
  return 0;
}

📝 说明

对于 B,我们不写构造函数,编译器会默认生成 —— 内置类型不处理,自定义类型会去调用它的默认构造函数处理 (无参的、全缺省的、编译器默认生成的),注意无参的和全缺省的只能存在一个,如果写了编译器就不会生成,如果不写编译器会默认生成。这里 C++ 有一个不好的处理 —— 内置类型不处理,自定义类型处理。针对这种问题,在 C++11 又打了一个补丁 —— 在内置类型后可以加上一个缺省值,你不初始化它时,它会使用缺省值初始化。这是 C++ 早期设计的缺陷。

class A
{
public:
  A(int a = 0)
  {
    _a = a; 
    cout << "A(int a = 0)" << endl;
  }
  A& operator=(const A& aa)//不写也行,因为这里只有内置类型,默认生成的就可以完成
  {
    cout << "A& operator=(const A& aa)" << endl;
    if(this != &aa)
    {
      _a = aa._a;
    }
    return *this;
  }
private:
  int _a;
};
class B
{
public:
  B(int a, int b)
  {
    //_aa._a = a;//err:无法访问private成员
    /*A aa(a);
    _aa = aa;*/ 
    _aa = A(a);//简化版,同上
    _b = b;
  }
private:
  int _b = 1;
  A _aa;
};
int main()
{
  B b(10, 20);
  return 0;
}

📝 说明

对上,_b只能初始化成1,_a只能初始化成0 ❓

这里可以显示的初始化,利用匿名对象来初始化 _a。

但是这种方法代价较大 (见下图)。

💦 初始化列表

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

class A
{
public:
  A(int a = 0)
  {
    _a = a; 
    cout << "A(int a = 0)" << endl;
  }
  A& operator=(const A& aa)
  {
    cout << "A& operator=(const A& aa)" << endl;
    if(this != &aa)
    {
      _a = aa._a;
    }
    return *this;
  }
private:
  int _a;
};
class B
{
public:
  B(int a, int b)
    :_aa(a)
  {
    _b = b;
  }
private:
  int _b = 1;
  A _aa;
};
int main()
{
  B b(10, 20);
  return 0;
}

📝说明

可以看到对比函数体内初始化,初始化列表初始化可以提高效率 —— 注意对于内置类型你使用函数体或初始化列表来初始化没有区别;但是对于自定义类型,使用初始化列表是更具有价值的。这里还要注意的是函数体内初始化和初始化列表是可以混着用的。

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

什么成员是必须使用初始化列表初始化的 ❓

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

⚠ 注意

1️⃣ 每个成员变量在初始化列表 (同定义) 中只能出现一次 (初始化只能初始化一次)。

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

  1、引用成员变量 (引用成员必须在定义的时候初始化)

  2、const 成员变量 (const 类型的成员必须在定义的时候初始化)

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

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

#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  随机值

如上程序的输出结果是 D 选项,因为 C++ 规定成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中出现的先后次序无关。实际中,建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。

💦 explicit关键字

class A
{
public:
  A(int a)
    :_a(a)
  {
    cout << "A(int a)" << endl; 
  }
  A(const A& aa)
  {
    cout << "A(const A& aa)" << endl; 
  }
private:
  int _a;
};
int main()
{
  A aa1(1);
  A aa2 = 1;
  return 0;
}

📝 说明

A aa2 = 1; 同 A aa1(1); 这是 C++98 支持的语法,它本质上是一个隐式类型转换 —— 将 int 转换为 A,为什么 int 能转换成 A 呢 ? —— 因为它支持一个用 int 参数去初始化 A 的构造函数。它俩虽然结果是一样的,都是直接调用构造函数,但是对于编译器而言过程不一样。

🍳验证

🔑拓展

针对于编译器优化、底层机制这类知识可以去了解一下《深度探索C++对象模型》

❓ 如果不想允许这样的隐式类型转换的发生 ❔

这里可以使用关键字 explicit

explicit A(int a)
  :_a(a)
{
  cout << "A(int a)" << endl; 
}

error C2440:无法从 int 转换成 A

❓ 多参数隐式类型转换 ❔

class A
{
public:
  A(int a1, int a2)
    :_a(a1)
  {
    cout << "A(int a1, int a2)" << endl;  
  }
  A(const A& aa)
  {
    cout << "A(const A& aa)" << endl; 
  }
private:
  int _a;
};
int main()
{
  A aa1(1, 2);
  //A aa2 = 1, 2;//???
  A aa2 = {1, 2};
  return 0;
}

📝说明

A aa2 = 1, 2; ???

明显 C++98 不支持多参数的隐式类型转换,但是 C++11 是支持的 —— A aa2 = {1, 2}; ,同样编译器依然会优化。

当我们使用 explicit 关键字限制时,它会 error C2440:无法从 initializer-list 转换为 A

二、static成员

💦 概念

❓ 写一个程序,计算程序构造了多少个对象 (构造+拷贝构造) ❔

int countC = 0;
int countCC = 0;
class A
{
public:
  A()
  {
    ++countC;
  }
  A(const A& a)
  {
    ++countCC;
  }
};
A f(A a)
{
  A ret(a);
  return ret;
}
int main()
{
  A a1 = f(A());
  A a2;
  A a3;
  a3 = f(a2);
  cout << countC << endl;
  cout << countCC << endl;
  return 0;
}

📝说明

这样虽然能计算出结果,但是有一个问题,countC 和 countCC 是可以随便改的,这样就很不好。

优化 ❓

class A
{
public:
  A()
  {
    ++_count;
  }
  A(const A& a)
  {
    ++_count;
  }
  int GetCount()
  {
    return _count;  
  }
  static int GetCount()
  {
    return _count;  
  }
private:
  int _a;
  static int _count;
};
//定义初始化
int A::_count = 0;
A f(A a)
{
  A ret(a);
  return ret;
}
int main()
{
  A a1 = f(A());
  A a2;
  A a3;
  a3 = f(a2);
  cout << sizeof(A) << endl;
  //这里就体现了static成员属于整个类,也属于每个定义出来的对象共享,但限制于公有
  /*cout << A::_count << endl;  
  cout << a1._count << endl;
  cout << a2._count << endl;*/
  /*A ret;
  cout << ret.GetCount() - 1 << endl;*/
  /*cout << A().GetCount() - 1 << endl;*/
  cout << A::GetCount() << endl;
  return 0;
}

📝说明

int _a; 存在定义出的对象中,属于对象。

static int _count; 存在静态区,属于整个类,也属于每个定义出来的对象共享。跟全局变量比较,它受类域和访问限定符限制,更好的体现封装,别人不能轻易修改。

static成员 ❓

对于非 static 成员它们的定义是在初始化列表中,但在 C++ 中,static 静态成员变量是不能在类的内部定义初始化的,这里的内部只是声明。注意这里虽然是私有成员,但是对于 static 成员它支持在外部进行定义,且不需要加上 static,sizeof 在计算的时候并不会计算 static 成员的大小。

_count是私有,怎么访问 ❓

定义一个公有函数 GetCount 函数,返回 _count:

调用,

1、最后实例化对象后调用 GetCount 函数并减 1

2、直接匿名对象并减 1

3、将 GetCount 函数定义成静态成员函数并使用类域调用

💦 特性

1️⃣ 静态成员变量为所有类对象所共享,不属于某个具体的实例。

2️⃣ 静态成员变量必须在类外定义,定义时不添加 static 关键字。

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

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

5️⃣ 静态成员和类的普通成员一样,也有 public、protected、private 3 种访问级别,也可以具有返 回值。

【面试题1】

static 的作用 C 语言 | C++ ❓

C 语言:

1、 static 修饰局部变量,改变了局部变量的生命周期 (本质上是改变了变量的存储类型),局部变量由栈区转向静态区,生命周期同全局变量一样

2、 static 修饰全局变量,使得这个全局变量只能在自己所在的文件内部使用,而普通的全局变量却是整个工程都可以使用

  ❓ 为什么全局变量能在其它文件内部使用 ❔

    因为全局变量具有外部链接属性;但是被 static 修饰后,就变成了内部链接属性,其它源文件不能链接到这个静态全局变量了

3、static 修饰函数,使得函数只能在自己所在的文件内部使用,本质上 static 是将函数的外部链接属性变成了内部链接属性 (同 static 修饰全局变量)

C++:

1、修饰成员变量和成员函数,成员变量属于整个类,所有对象共享,成员函数没有 this 指针。

【面试题2】

静态成员函数可以调用非静态成员函数吗 ❓

不能,因为静态成员函数没有 this 指针。

非静态成员函数可以调用静态成员函数吗 ❓

可以,因为非静态成员函数有 this 指针。


相关文章
|
1天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
10 4
|
24天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
22 4
|
24天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
20 4
|
24天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
18 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
51 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
18 1
|
1月前
|
C++
C++番外篇——日期类的实现
C++番外篇——日期类的实现
81 1