【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 指针。


相关文章
|
12天前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
55 6
|
4月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
170 5
|
5月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
54 3
|
1月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
8天前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
37 16
|
1月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
1月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
2月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
85 19
|
2月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
83 13

热门文章

最新文章