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

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

二、隐式类型转换

1、概念

隐式类型转换是指当两个不同类型的变量之间进行运算时,编译器会自动将其中一个变量的类型转换为另一个变量的类型;在之前 C++引用 的常引用中我们也接触过,比如:

int main()
{
  int a = 0;
  double b = a;
  const double& rb = a;
    const int& c = 1;
}

如上,如果我们将 int 的 a 赋值给 double 的 b,则 a 不会被直接赋值给 b,编译器会先根据 a 创建一个 double 类型的临时变量 tmp,然后将 tmp 赋值给 b;


对于 rb 来说也是一样的,只不过 rb 是引用类型,而引用和指针需要考虑权限的问题,所以用引用类型的变量 rb 去引用 a 生成的临时变量 tmp 需要使用 const 修饰 (临时变量具有常性);


对于最后一条语句来说也大同小异 – 由于数字 1 只存在于指令中,在内存中并不占用空间,所以当我们对其进行引用时,10会先赋给一个临时变量,然后我们再对这个临时变量进行引用;同时由于临时变量具有常性,所以我们需要使用 const 修饰;

上面这些知识在我前面 C++引用 的常引用部分有详细的介绍,如果有遗忘的小伙伴可以去复习一下;

2、构造函数的类型转换

在C++98中,单参数的构造函数也支持隐式类型转换,即对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,其具有类型转换的作用;比如下面这样:

class Date
{
public:
  Date(int year)
    :_year(year)
  {
    cout << "Date 构造" << endl;
  };
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    cout << "Date 拷贝构造" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

2020062310470442.png

20200623104134875.png

如上,对于具有单参构造函数的Date类,我们不仅可以使用构造和拷贝构造的方式来实例化对象,还可以通过直接赋值一个整数来实例化对象;


而这其实是隐式类型转换的结果:


对于d3来说,由于2022和d3的类型不同,所以编译器会进行类型转换,即先使用2022来构造一个临时的Date对象,然后用这个临时对象来对d3进行拷贝构造,所以d3是构造+拷贝构造的结果;


不过现在比较新版的编译器都对这种情况进行了优化,不再创建临时的Date对象,而是直接使用2022来构造d3,所以我们看到的现象是创建d3没有调用拷贝构造函数;而在老版的编译器下是会调用拷贝构造函数的,比如VC 6.0、VS2003等编译器;


对于d4来说,d4是Date对象的引用,所以编译器会先用2022来构造一个Date类型的临时对象,然后d4再对这个临时对象进行引用,所以只会调用一次构造函数;同时由于临时对象具有常性,所以需要使用const修饰;

注意:单参构造函数并不是指只能有一个参数,而是指在调用时只传递一个参数,所以半缺省和全缺省的构造函数也是可以的;

2020062310470442.png

C++11对上述语法进行了拓展,支持多参数的构造函数,只是传递参数时多个参数之间需要使用花括号,如下:

2020062310470442.png

20200623104134875.png

3、explicit 关键字

explicit 关键字用于修饰构造函数,其作用是禁止构造函数的隐式类型转换,如下:

class Date
{
public:
  explicit Date(int year = 1970, int month = 1, int day = 1)
    :_year(year)
    , _month(month)
    , _day(day)
  {
    cout << "Date 构造" << endl;
  };
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    cout << "Date 拷贝构造" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

2020062310470442.png

4、类型转换的意义

构造函数的类型转换在一些特定的地方有着很大的意义,比如要在一个顺序表尾插一个元素,而这个元素是 string 类型的对象,如下:

int main()
{
  string s1("hello");
  push_back(s1);
  push_back("hello");
}

如上,有了隐式类型转换我们在插入一个 string 对象时就不必先去构造 s1,然后再传递 s1,而是直接使用 “hello” 做参数即可,其会自动转化为 string 类型;等等类似的情景还有非常多,具体的我们在后面遇到再说;

三、static 成员

1、概念

声明为 static 的类成员称为类的静态成员,其中用 static 修饰的成员变量,称之为静态成员变量,用 static 修饰的成员函数,称之为静态成员函数;下面我们以一个面试题来引出类的静态成员的相关知识点;

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

我们知道,类创建对象一定会调用构造函数或者拷贝构造函数,所以我们只需要定义一个全局变量,然后在构造函数和拷贝构造函数中让其自增即可,如下:

int N = 0;
class A
{
public:
  A(int i = 0)
    :_i(i)
  {
    N++;
  }
  A(const A& a)
  {
    _i = a._i;
    N++;
  }
private:
  int _i;
};

2020062310470442.png

虽然使用全局变量的方法可以十分简便的达到我们的目的,但是我们不建议使用全局变量,因为全局变量可以被任何人修改,十分不安全;所以我们需要使用另外一种比较安全的方法 – 静态成员变量;

2、static 成员变量

静态成员变量是指用 static 关键字修饰的成员变量,其特性如下:

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
  • 静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明;
  • 静态成员变量的访问受类域与访问限定符的约束;

接下来我们围绕这三点特性来展开说明:

1、由于静态成员变量在静态区 (数据段) 开辟空间,并不在对象里面,所以它不属于某单个对象,而是所有对象共享;

class A
{
public:
  A(int m = 0)
    :_m(m)
  {}
public:
  int _m;
  static int _n;
};
int A::_n = 0;

2020062310470442.png

可以看到,当我们把类的静态成员变量设置为 public 并在类外进行定义初始化后,我们可以直接通过类名+域作用限定符或者通过一个空指针对象来访问,这说明_n并不存在于对象里面;


2、类的静态成员变量在类中只是声明,必须在类外进行定义且定义时需要指定类域,其不在初始化列表处进行定义初始化,因为新建对象并不会改变它的值;

2020062310470442.png

20200623104134875.png

tips:当我们的程序出现错误时,输出列表提供的错误信息是最准确的,且我们应该从第一个错误开始解决;

3、静态成员变量的访问受类域与访问限定符的约束;

2020062310470442.png

20200623104134875.png

如上,静态成员变量在访问时和普通的成员变量区别不大,同样受类域和访问限定符的约束,只是因为其不存在于对象中,所以我们可以通过 A:: 来直接访问;


注:可以看到,静态成员变量在定义声明的时候只受类域的限制,而没有受到访问限定符的限制,这是一个特例,大家记住即可;


学习了静态成员变量的相关知识以后,我们就可以换一种方式来统计对象的创建个数了:

class A
{
public:
  A(int i = 0)
    :_i(i)
  {
    _n++;
  }
  A(const A& a)
  {
    _i = a._i;
    _n++;
  }
private:
  int _i;
  static int _n;
};
int A::_n = 0;

2020062310470442.png

但是这里出现了一个新的问题:为了保证类的封装性我们需要将成员变量设置为private,但是这样又使得我们无法在类外获取到它们,那该怎么办呢?针对这个问题,C++设计出了静态成员函数;

3、static 成员函数

静态成员函数是指用 static 关键字修饰的成员函数,其特性如下:

  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
  • 静态成员也是类的成员,同样受类域和访问限定符的约束;

由于静态成员函数没有隐藏的 this 指针,所以我们在调用的时候自然也就不需要传递对象的地址,即我们可以通过类名+域作用限定符直接调用,而不需要创建对象;但是相应的,没有了 this 指针我们也无法去调用非静态的成员变量与成员函数,因为非静态成员变量需要实例化对象来开辟空间,非静态成员函数的调用则需要传递对象的地址;

class A
{
public:
  A(int i = 0)
    :_i(i)
  {
    _n++;
  }
  A(const A& a)
  {
    _i = a._i;
    _n++;
  }
  static int GetN()
  {
    return _n;
  }
private:
  int _i;
  static int _n;
};
int A::_n = 0;

2020062310470442.png

注意:虽然静态成员函数函数不可以调用非静态成员,但是非静态成员函数是可以调用静态成员的 (调用静态成员时编译器不传递对象地址即可);


最后,让我们来做一道与静态成员相关的练习题:求1+2+3+…+n


求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

class Sum
{
public:
    Sum()
    {
        ++_i;
        _ret += _i;
    }
    static int GetRet()
    {
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};
int Sum::_i = 0;
int Sum::_ret = 0;
class Solution {
public:
    int Sum_Solution(int n) {
        Sum arr[n];
        return Sum::GetRet();
    }
};



目录
打赏
0
0
0
0
1
分享
相关文章
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
47 12
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
55 16
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
2月前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
131 6
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
4月前
|
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
119 19
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等