【C++】类和对象(下)

简介: 【C++】类和对象(下)

前言

本篇文章是类和对象部分的收官之作,主要讲解初始化列表、构造函数的一些补充知识,static成员,上篇文章提到的友元函数,内部类以及如何理解封装。


欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

=========================================================================


1.初始化列表

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

但这样的初始化形式难免会遇到一些不能解决的类型,比如引用成员变量和const成员变量以及自定义类型成员(且该类没有默认构造函数时)。

如下图所示,该位置的意义是成员变量的声明而不是定义。


那C++中我们都知道应该是在创建对象时整体定义的。

但是每个成员变量是在什么地方定义的呢?

如果像是引用成员变量和const成员变量这种必须在定义时就初始化的变量怎么办?

这是C++引用了初始化列表这一概念。

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. 类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)

class A
{
public:
  A(int a)//需要传参的构造函数不是默认构造函数哦!
    :_a(a)
  {}
private:
  int _a;
};
class B
{
public:
  B(int a, int ref)
    :_aobj(a)//_aobj初始化为a    就不需要默认构造函数了
    , _ref(ref)//_ref初始化为ref
    , _n(10)//n初始化为10
  {}
private:
  A _aobj; // 没有默认构造函数
  int& _ref; // 引用
  const int _n; // const
};

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

并且以前的这种写法,这个地方的缺省值是给初始化列表的。


注意:每个成员变量都会在初始化列表定义,不管你再初始化列表里写没写,未指定时,默认内置类型赋为随机值,自定义类型会去调用默认构造。


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

如定义所讲,成员变量在类中的生命次序就是初始化列表中的初始化顺序,所以根据声明次序,初始化列表中先执行_a2(_a1) ,再执行_a1(a),导致在执行_a2(_a1) 这句时,_a1的值还未可知,所以导致最后的结果为D。

  • 我们建议声明和初始化列表顺序保持一致,避免出现理解问题。

那讲到这,很多同学会有疑惑,既然初始化列表这么好用,函数体初始化还有存在的必要么?

  • 有必要!

因为有些初始化或者检查(malloc、memset、perror)工作,初始化列表也不能全部搞定。

建议80%-100%初始化列表搞定,剩下配合函数体初始化使用。


1.3explicit关键字

请看下面的代码:

A aa1 = 1;

这中间的过程是怎样的呢?

实际上发生了一次隐式类型转换

编译器会先对1构造一个临时对象,然后再将该临时对象拷贝构造给aa1。


💥但是这个过程是有前提的💥

前提是由A类的单参数构造函数(只有一个参数或者是多参缺省)支持。

比如:

class A {
  A(int a)//单参数构造函数
    :_a(a)
  {}
};
class A {
  A(int a,int b=1,int c=1)// 多参缺省构造函数
    :_a(a)
        ,_b(b)
        ,_c(c)
  {}
};

如果不想让转换发生,就需要在构造函数前加explicit关键字

比如:

class A {
  explicit A(int a)
    :_a(a)
  {}
};

2.Static成员

2.1概念

声明为static的类成员称为类的静态成员:

  • 用static修饰的成员变量,称之为静态成员变量;
  • 用static修饰的成员函数,称之为静态成员函数。

静态成员变量一定要在类外进行初始化。

静态成员函数和静态成员变量,本质是受限制的全局变量和全局函数,受类域和访问限定符的限制。

2.2特性

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

一个小知识:类名()这种写法叫做匿名对象,他的生命周期只在这一行,如A()。

来看两道问题:

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

答:不能,因为没有this指针。

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

答:可以。


3.友元

友元提供了一种突破封装的方式,有时提供了便利。

但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

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

3.1友元函数

在运算符重载那一篇文章中📢樊梓慕->运算符重载,我们提到过友元函数的概念。

我们知道一般的运算符重载我们可以放到类内部实现来避免成员变量私有的问题。

但当我们想要重载流运算符时却遇到了问题,因为如果流运算符也在类内部重载的话,this指针为首个参数,这样和流运算符的使用方法又不相符,所以我们尝试将其放到全局来重载,那我们如何解决成员变量私有的问题呢?

下面的代码就反应了这种尴尬场景:

class Date
{
public:
  Date(int year, int month, int day)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  // d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
  // 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
  ostream& operator<<(ostream& _cout)
  {
    _cout << _year << "-" << _month << "-" << _day << endl;
    return _cout;
  }
private:
  int _year;
  int _month;
  int _day;
};

所以我们需要友元来解决这一问题。

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

用法如下:

class Date
{
  friend ostream& operator<<(ostream& _cout, const Date& d);//友元的声明
  friend istream& operator>>(istream& _cin, Date& d);
public:
  Date(int year = 1900, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)//友元的定义
{
  _cout << d._year << "-" << d._month << "-" << d._day;
  return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
  _cin >> d._year;
  _cin >> d._month;
  _cin >> d._day;
  return _cin;
}
int main()
{
  Date d;
  cin >> d;
  cout << d << endl;
  return 0;
}

友元函数的使用需要注意以下问题:

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

3.2友元类

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

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

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

(Time说(声明)Date是我的朋友,他可以来我家玩,但是Date并不一定这么认为)

2.友元关系不能传递

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

3.友元关系不能继承


4.内部类

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

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员

外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。(外部类认为内部类是他的朋友,但内部类并不这么认为)

特性:

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。

5.编译器对连续构造、拷贝构造次数的优化

较为新版的编译器会对构造、拷贝构造的次数进行优化。

也就是说他会较为智能的节省拷贝与拷贝构造的次数。

注意:本篇文章所讲的编译器优化不适用所有编译器,只是一般情况,还有一些编译器跨表达式也可以优化。

比如:

同一个表达式中(需要特别注意是在同一个表达式中)

  • 构造+构造 -> 构造
  • 构造+拷贝构造 -> 构造
  • 拷贝构造+拷贝构造 -> 拷贝构造

下面我已经构建好了一些优化的场景,大家可以学习下。

class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a)" << endl;
  }
  A(const A& aa)
    :_a(aa._a)
  {
    cout << "A(const A& aa)" << endl;
  }
  A& operator=(const A& aa)
  {
    cout << "A& operator=(const A& aa)" << endl;
    if (this != &aa)
    {
      _a = aa._a;
    }
    return *this;
  }
  ~A()
  {
    cout << "~A()" << endl;
  }
private:
  int _a;
};
void func1(A aa1) {}
A func2()
{
  A aa;
  return aa;
}
int main()
{
  // 1、先用1构造一个临时对象,再用临时对象拷贝构造aa1
  A aa1 = 1;// 构造+拷贝构造->构造
  // 2、先用2构造一个临时对象,再用临时对象拷贝构造aa2
  //这个例子证明了优化是现实存在的
  //因为aa2不能直接引用常量,所以这里实际上先构造了一个临时对象,然后将该临时对象拷贝构造给aa2
  const A& aa2 = 2; // 构造+拷贝构造->构造
  //3、未在同一表达式,不优化(跨表达式在一些编译器上也会优化,但建议大家还是默认不优化)
  A aa(1);//构造
  func1(aa);//拷贝构造
  //4、先构造了一个临时对象,然后将该临时对象拷贝构造给形参aa1
  func1(A(2));//构造+拷贝构造->构造 
  //5、单参数传参支持这样写,先构造一个临时对象,再将该临时对象拷贝构造给形参aa1
  func1(3);//构造+拷贝构造->构造 
  //6、函数内部,先构造aa,然后调用拷贝构造保存aa的值,出作用域析构,将之前拷贝构造再拷贝构造给aa3
  A aa3 = func2();//拷贝构造+拷贝构造->拷贝构造
  /*********************************注意***********************************/
  A aa4(aa1);  // 拷贝构造
  A aa5 = aa1; // 拷贝构造 or 赋值拷贝
  // 两个已经存在的对象拷贝,赋值拷贝
  aa4 = aa5;
  return 0;
}

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

目录
相关文章
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
84 0
|
3月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
165 0
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
161 12
|
6月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
124 16
|
6月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
6月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
6月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
325 6
|
6月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
7月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)