【C++初阶】类和对象(三)(上)

简介: 【C++初阶】类和对象(三)(上)

1.const 成员

若定义了一个 const 的对象,然后访问其成员函数,会报错,这是为什么?

因为在传参时,d2 的地址 &d2 会被传递给 Print() ,作为隐藏的参数 this 指针:

void Print(Date* const this) // this 指针隐藏
{
    cout << _year << '-' << _month << '-' << _day << endl;
}

对于 Date d1 ,传递过去的 &d 是 Date* ;而 const Date d2 ,传递过去的 &d2 是 const Date* .

fbdae65a45444049a762b035812dc781.png

对于 this 指针本身是 Date* const this ,此刻 const 修饰的是 this ,this 不可改,但是 * this 是可改的 。

而传参时传过来的 &d2 为 const Date* ,这时 const 修饰指针指向的内容,即对象本身不可改了。


但对于Print()函数的 this 来说,*this,也就是指向的内容,即对象本身是可改的,但是现在由于 const 使得指向内容不可改,对于权限来说,只能对等和缩小,但是const Date d2在传递时权限放大了,所以报错 。

为了解决这一问题,C++ 引入了 const 成员 ,在该成员后加上 const :

a05597c4a393493a890ab26657e51eff.png

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

void Print() const
{
    cout << _year << '-' << _month << '-' << _day << endl;
}

这时 this 指针的类型变为 const Date* const this ,权限对等了,这时 this 指针不能改,且 *this ,即 this 指向的对象也不能改,和 const Date d2 的目的相同:不可改 d2 。 这时,就不会报错了。


而对于 d1 对象,它虽然没有 const ,但是也只是 权限缩小,使得 d1 在 Print() 成员函数中不可修改而已,也是没问题的。

0286756a689740c383c13e14835a188c.png

总结:成员函数加上 const 是好的,建议能加上 const 都加上。这样普通对象和 const 对象,都可以调用。但是如果对于要对 对象 进行修改的成员函数不要加上,不然就完成不了目的了。

f7540994e097449c8e5571c3163e1b40.png

注:对于构造和析构不能加上const修饰。

2.取地址与const取地址操作符重载

我们知道,对于自定义类型成员来说,平常的操作符需要重载后才能对对象进行操作。但是对于自定义类型的对象来说,如果不写这两个成员函数,使用默认的成员函数照样也可以完成目的:image.pngimage.png所以一般不写,但是写的话也可以:

class Date
{
public:
  Date* operator&()
  {
    return this;
  }
  const Date* operator&() const
  {
    return this;
  }
};

image.png就只要返回 this 就可以;对于 const 取地址操作符,则要加上 const 成员,并且返回的指针也要加上 const 修饰。

f8be72142ef9410eb97d2ac3628d7e41.png取地址与const取地址操作符重载:

可以直接取出成员的地址,一般不自己写

运用场景:使其取不到地址386b6e972f954ea8ad2e9a4de3f74dd5.png

总结: 对于六个默认成员函数,前四个最重要:构造、析构、拷贝构造、运算符重载。后两个有一定作用,但是作用不大。

3. 再谈构造函数

3.1 构造函数体赋值

在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:

class Date
{
public:
  // 构造函数
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
class Date

注意:虽然通过调用上述的构造函数后,对象中的每个成员变量都有了一个初始值,但是构造函数中的语句只能将其称作为赋初值,而不能称作为初始化。因为初始化只能初始化一次,而构造函数体内可以进行多次赋值。

class Date
{
public:
  // 构造函数
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;// 第一次赋值
    _year = 2022;// 第二次赋值
    //...
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

3.2 初始化列表

3.2.1 定义

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

class Date
{
public:
  // 构造函数
  Date(int year = 0, int month = 1, int day = 1)
    :_year(year)
    , _month(month)
    , _day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};

3.2.2 注意事项

1.每个成员变量在初始化列表中最多只能出现一次

 因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。583ff6f86cb74f87be5c017b361976d1.png2.类中包含以下成员,必须放在初始化列表进行初始化

2.1 引用成员变量

 引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。

int a = 10;
  int& b = a;// 创建时就初始化

2.2 const成员变量

 被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。

const int a = 10;//correct 创建时就初始化
  const int b;//error 创建时未初始化

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

 若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。


默认构造函数是指不用传参就可以调用的构造函数:

 1.编译器自动生成的构造函数。

 2.无参的构造函数。

 3.全缺省的构造函数。

class A //该类没有默认构造函数class A //该类没有默认构造函数 
{
public:
  A(int val) //注:这个不叫默认构造函数(需要传参调用)
  {
    _val = val;
  }
private:
  int _val;
};
class B
{
public:
  B()
    :_a(2021) //必须使用初始化列表对其进行初始化
  {}
private:
  A _a; //自定义类型成员(该类没有默认构造函数)
};

总结:在定义时必须进行初始化的变量类型,就必须放在初始化列表进行初始化。


三、尽量使用初始化列表初始化

 因为初始化列表实际上就是当实例化一个对象时,该对象的成员变量定义的地方,所以无论是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)


严格来说:

 1.对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:

// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;

2.对于自定义类型,使用初始化列表可以提高代码的效率

class Time
{
public:
  Time(int hour = 0)
  {
    _hour = hour;
  }
private:
  int _hour;
};
class Test
{
public:
  // 使用初始化列表
  Test(int hour)
    :_t(12)// 调用一次Time类的构造函数
  {}
private:
  Time _t;
};

当我们要实例化一个Test类的对象时,我们使用了初始化列表,在实例化过程中只调用了一次Time类的构造函数。

我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:

class Time
{
public:
  Time(int hour = 0)
  {
    _hour = hour;
  }
private:
  int _hour;
};
class Test
{
public:
  // 在构造函数体内初始化(不使用初始化列表)
  Test(int hour)
  { //初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程)
    Time t(hour);// 调用一次Time类的构造函数
    _t = t;// 调用一次Time类的赋值运算符重载函数
  }
private:
  Time _t;
};

这时,当我们要实例化一个Test类的对象时,在实例化过程中会先在初始化列表时调用一次Time类的构造函数,然后在实例化t对象时调用一次Time类的构造函数,最后还需要调用了一次Time类的赋值运算符重载函数,效率就降下来了。


3.初始化列表虽好,但有些地方还是需要函数体赋值,比如判断开辟空间是否成功·b64d998b78654c93b6e2ce8f73d2cfb0.png还要一些工作是初始化列表做不完的,比如动态开辟二维数组image.png

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

#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
  Test()
    :_b(i++)
    ,_a(i++)
  {}
  void Print()
  {
    cout << "_a:" << _a << endl;
    cout << "_b:" << _b << endl;
  }
private:
  int _a;
  int _b;
};
int main()
{
  Test test;
  test.Print(); //打印结果test._a为0,test._b为1
  return 0;
}

代码中,Test类构造函数的初始化列表中成员变量_b先初始化,成员变量_a后初始化,按道理打印结果test._a为1,test._b为0,但是初始化列表的初始化顺序是成员变量在类中声明次序,所以最终test._a为0,test._b为1。

 例题:e332a327fbfb4068b704b0dfff2628c3.png

答案:D,按申明的顺序,先初始化_a2,为随机值,后初始化_a1,为1

dc17aaea41404776a5f24d83247cad65.png所以在写程序时要尽量按照申明的顺序初始化,否则容易入坑:ed3cbc72061f4f58ac07986c1334b40e.png运行以上程序,会崩溃,因为按申明的顺序,先初始化_a,但此时capacity还未初始化,为随机值,开辟的空间太大,程序崩溃。

五、到底是否使用初始化列表,具体问题具体分析image.png

3.3 explicit关键字

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换。

#include <iostream>
using namespace std;
class A
{
public:
   A(int a) //单个参数的构造函数
    :_a(a)
  {
  cout << "A(int a)" << endl;
  }
private:
  int _a;
};
int main()
{
  A aa1 (1);
  A aa2 = 2;//隐式类型转换
  return 0;
}

在语法上,代码中A aa2 = 2等价于以下两句代码:

Date tmp(2); //先构造
Date aa2(tmp); //再拷贝构造

在早期的编译器中,当编译器遇到 A aa2 = 2 这句代码时,会先构造一个临时对象(临时对象具有常性),再用临时对象拷贝构造 aa2;但是现在的编译器已经做了优化,当遇到 A aa2 = 2这句代码时,会按照 A aa2 (2)这句代码处理,这就是隐式类型转换。image.png实际上,我们早就接触了隐式类型转换:

int i = 10;
double d = i; //隐式类型转换

在这个过程中,编译器会先构建一个double类型的临时变量接收i的值,然后再将该临时变量的值赋值给d。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。


但是,对于单参数的自定义类型来说,A aa2 = 2 这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。

99fe70b188504828b65c95abbf27561e.png

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