C++编程之美:探索初始化之源、静态之恒、友情之桥与匿名之韵

简介: C++编程之美:探索初始化之源、静态之恒、友情之桥与匿名之韵


一、初始化列表

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

注意:

      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 Time
{
public:
  Time(int hour = 0)
    :_hour(hour)
  {
    cout << "Time()" << endl;
  }
private:
  int _hour;
};
class Date
{
public:
  Date(int day)
  {}
private:
  int _day;
  Time _t;
};
int main()
{
  Date d(1);
}

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

举例:

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,初始化列表初始化的是声明的顺序

1.3 explicit关键字

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

  • 单参数
class Date
{
public:
    // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
    Date(int year)
    :_year(year)
  {}
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
   Date d1(2022);//实例化
     Date  d2=2024;//实际编译器背后会用整型2024构造一个临时对象(隐式类型转化),最后用临时对象给d2对象进行拷贝构造
  
  const Date& d3= 2023;//2023构造的临时对象具有常性,所以引用要加const
  return 0;
}

<1>  Date  d2=2024 , 实际编译器背后会用整型2024构造一个临时对象(隐式类型转化),最后用临时对象给d2对象进行拷贝构造。

<2> const Date& d3= 2023整型2023构造一个临时对象(隐式类型转化)具有常性,所以引用要加const.

explicit Date(int year):_year(year)

<3> explicit修饰构造函数,将会禁止构造函数的隐式转换。

  • 第一个参数无默认值其余均有默认值的构造函数
#include<iostream>
using namespace std;
class Date
{
public:
  //2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
  //有类型转换作用
  Date(int year, int month = 1, int day = 1)
  : _year(year)
  , _month(month)
  , _day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1 = 2023;
  const Date& d2 = 2024;
  return 0;
}
  • 多参数(补充:C++11标准)
#include<iostream>
using namespace std;
class Date
{
public:
  //c++11标准,多参数,没有使用explicit修饰,具
  //有类型转换作用
  Date(int year, int month)
  : _year(year)
  , _month(month)
  , _day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1 = {2023,1};
  const Date& d2 ={ 2024,2};
  return 0;
}
  • c++11标准,多参数传参,没有使用explicit修饰,也具有隐式类型转化。

二、static成员

2.1 概念

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

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

class A {
public:
  A() {
    n++;
  }
  A(const A& aa) {
    n++;
  }
  static int GetN() {
    return n;
  }
private:
  static int n;//声明
};
int A::n = 0;//相当于静态的全局的
A func() {
  A aa;
  return aa;
}
int main() {
  A aa1;
  A aa2;
  func();
  cout << A::GetN() << endl;
  return 0;
}

答案:4

一共构造了aa1,aa2,aa三个对象,aa传值返回会调用拷贝构造创建一个临时对象。

2.2 特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

问题:

  1. 静态成员函数可以调用非静态成员函数吗?              不能直接调用
  2. 非静态成员函数可以调用类的静态成员函数吗?               可以!

三、友元

友元提供了一种突破封装(访问限定符)的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:友元函数友元类

3.1 友元函数

3.1.1 流插入运算符重载
  • 对于内置类型可以流插入,那么自定义类型也可以流插入
  • 对于流插入【cout】,属于ostream类

#include<iostream>
using namespace std;
class Date {
public:
  Date(int year=2024, int month=2, int day=11)
     : _year(year)
     , _month(month)
     , _day(day)
     {}
  void operator<<(ostream& out) {
    out << _year << '-' << _month << '-' << _day << endl;
  }
private:
  int _day;
  int _month;
  int _year;
};
int main() {
  Date d1;
    //本末倒置
  d1 << cout;
  return 0;
}

这里我们在使用的时候,为d1<<count,为什么呢?

因为作为成员函数重载,this指针占据第一个参数,void operator<<(ostream& out)=void operator<<(Date * this,ostream& out),  d1为左操作数了,但是实际使用中cout需占据第一个形参,所以要将operator<<重载成全局函数。但又会导致类外没办法访问类的成员,此时就需要友元来解决。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

#include<iostream>
using namespace std;
class Date {
public:
  Date(int year=2024, int month=2, int day=11)
     : _year(year)
     , _month(month)
     , _day(day)
     {}
  friend void operator<<(ostream& out,const Date &d); 
private:
  int _day;
  int _month;
  int _year;
};
void operator<<(ostream& out, const Date& d) {
  out << d._year << '-' << d._month << '-' << d._day << endl;
}
int main() {
  Date d1;
  cout << d1;
  return 0;
}

当然这里是多组流插入,为了更清晰易懂,我们将

void operator<<(ostream& out, const Date& d) {
  out << d._year << '-' << d._month << '-' << d._day << endl;
}

改为

ostream& operator<<(ostream& out, const Date& d) {
  out << d._year << '-' << d._month << '-' << d._day << endl;
  return out;
}

解释:<<是从左往右的顺序,d._year先流插入,得到一个返回类型为ostream的返回值out,d._month再向返回值out中插入

3.1.2 流提取运算符重载
  • 这里Date就不能使用const了,因为Date会变化
#include<iostream>
using namespace std;
class Date {
public:
  Date(int year=2024, int month=2, int day=11)
     : _year(year)
     , _month(month)
     , _day(day)
     {}
  friend ostream& operator<<(ostream& out,const Date& d);
  friend istream& operator>>(istream& in, Date& d);
  //不加const,因为要放到日期内进行运算
private:
  int _day;
  int _month;
  int _year;
};
ostream& operator<<(ostream& out,const Date& d) {
  out << d._year << '-' << d._month << '-' << d._day << endl;
  return out;
}
istream& operator>>(istream& in, Date& d) {
  cout << "请依次输入年月日:";
  in >> d._year >> d._month >> d._day;
  return in;
}
 
int main() {
  Date d1;
  Date d2;
  cin >> d1 >> d2;
  cout << d1 << d2;
  return 0;
}

说明:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰(因为友元函数没有this指针)
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

3.2  友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员

1、友元关系是单向的,不具有交换性。

       比如下面的Time类和Date类, 在Time类中声明Date类为其友元类 , 那么可以在Date类中直接访问Time类的私有成员变量, 但想在Time类中访问Date类中私有的成员变量则不行 。

2、友元关系不能传递

       如果C是B的友元, B是A的友元,则不能说明C时A的友元。

class Time
{
   friend class Date;   // 声明日期类为时间类的友元类,
                       //则在日期类中就直接访问Time类中的私有成员变量
public:
     Time(int hour = 0, int minute = 0, int second = 0)
     : _hour(hour)
     , _minute(minute)
     , _second(second)
     {}
   
private:
   int _hour;
   int _minute;
   int _second;
};
class Date
{
public:
   Date(int year = 1900, int month = 1, int day = 1)
       : _year(year)
       , _month(month)
       , _day(day)
   {}
   
   void SetTimeOfDate(int hour, int minute, int second)
   {
       // 直接访问时间类私有的成员变量
       _t._hour = hour;
       _t._minute = minute;
       _t._second = second;
   }
   
private:
   int _year;
   int _month;
   int _day;
   Time _t;
};

四、内部类

4.1 概念

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

  • 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
  • 2. 内部类天生就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
  • 3. 内部类就类似于全局域(只是受到类域和访问限定符的限制而已)

4.2 特性

特性:

1. 内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static成员不需要外部类的对象/类名

3. sizeof(外部类)=外部类,和内部类没有任何关系。

#include<iostream>
using namespace std;
class A {
public:
  class B {
  private:
    int _b1;
  };
private:
  int _a1;
  int _a2;
};
int main() {
  cout << sizeof(A) << endl;
  return 0;
}

答案是8,与内部类没有任何关系。

五、匿名对象

class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a)" << endl;
}
~A()
{
  cout << "~A()" << endl;
}
private:
  int _a;
};
class Solution {
public:
  int Sum_Solution(int n) {
    //...
    return n;
  }
};
int main()
{
  A aa1;
  // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
  //A aa1();
  // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
  // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
  
  //匿名对象
  A();
  A(10);
    return 0;
}

相关文章
|
5天前
|
算法 编译器 C语言
探索C++编程的奥秘与魅力
探索C++编程的奥秘与魅力
|
1月前
|
安全 算法 C++
【C/C++ 泛型编程 应用篇】C++ 如何通过Type traits处理弱枚举和强枚举
【C/C++ 泛型编程 应用篇】C++ 如何通过Type traits处理弱枚举和强枚举
48 3
|
5天前
|
编译器 C语言 C++
C语言,C++编程软件比较(推荐的编程软件)
C语言,C++编程软件比较(推荐的编程软件)
|
1月前
|
安全 程序员 编译器
【C/C++ 泛型编程 进阶篇 Type traits 】C++类型特征探究:编译时类型判断的艺术
【C/C++ 泛型编程 进阶篇 Type traits 】C++类型特征探究:编译时类型判断的艺术
176 1
|
1天前
|
安全 程序员 编译器
【C++类和对象】初始化列表与隐式类型转换
【C++类和对象】初始化列表与隐式类型转换
|
1天前
|
算法 程序员 C语言
C++:深度探索与编程实践
C++:深度探索与编程实践
10 3
|
6天前
|
编译器 程序员 C++
C++从入门到精通:3.1模板编程——提高代码的复用性和灵活性
C++从入门到精通:3.1模板编程——提高代码的复用性和灵活性
|
6天前
|
C++
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
|
7天前
|
编译器 C++ 容器
【C++11(一)】右值引用以及列表初始化
【C++11(一)】右值引用以及列表初始化
|
7天前
|
存储 编译器 对象存储
【C++基础(十)】C++泛型编程--模板初阶
【C++基础(十)】C++泛型编程--模板初阶
【C++基础(十)】C++泛型编程--模板初阶