第四章 类与对象
构造函数
构造函数基本概念
-
构造函数的作用
- 在对象被创建时使用特定的值构造对象,将对象初始化为一个特定的初始状态;
- 我们可以在类体中写一种特殊的函数,叫做构造函数,在构造函数中,我们就可以描述如何对类的对象进行初始化,把初始化的规则、算法写在构造函数中;
- 例如:希望在构造一个 Clock 类对象时,将初始时间设为0:0:0,就可以通过构造函数来设置。
-
构造函数的形式
- 函数名与类名相同;
- 不能定义返回值类型,也不能有 return 语句;
- 可以有形式参数,也可以没有形式参数;
- 可以是内联函数;
- 可以重载;
- 可以带默认参数值。
-
构造函数的调用时机
- 在对象创建时被自动调用,我们不需要在程序中写代码显式地调用构造函数;
- 例如:
Clock myClock(0, 0, 0) ;
我们就把初始值放在对象名后面的括号里面,如果我们在这个 Clock 类里面定义了构造函数,并且这个构造函数可以接收三个 int 类型的数据作为实参,那么在这个时刻构造函数就会被调用,如果在 Clock 类里面没有定义构造函数,那编译器就会报错说找不到这种形式的构造函数。
-
默认构造函数
-
我们定义构造函数的时候可以有参数表,也可以让参数表空着,也就是说,构造函数可以有参数也可以没参数,即使一个构造函数有参数,我们也可以给所有的参数都指定默认值,也就是说,实际在调用构造函数的时候,有可能你根本就不需要给初始化参数;
-
如果一个构造函数它在被调用的时候不需要实参,我们就称这个构造函数是一个默认的构造函数;
-
默认构造函数是调用时可以不需要实参的构造函数;
-
默认构造函数可能是参数表为空的构造函数;
-
默认构造函数可能是全部参数都有默认值的构造函数;
-
下面两个都是默认构造函数,如在类中同时出现,将产生编译错误:
Clock(); Clock(int newH=0, int newM=0, int newS=0);
-
-
隐含生成的构造函数
- 构造一个新对象的时候,调用构造函数是必须的,不可能说构造一个新对象,但是不调用任何构造函数;
- 如果说我们定义一个类的时候不声明任何构造函数,那么编译器在编译的时候就会为我们自动生成一个构造函数,这个生成的构造函数是一个隐含的默认的构造函数;
- 这个隐含的默认的构造函数的参数表是空的,所以我们不能通过这个默认构造函数
为数据成员设置初始值; - 如果类内定义了成员的初始值,则使用内类定义的初始值;
- 如果没有定义类内的初始值,则以默认方式初始化,即基本类型的数据默认初始化的值是不确定的;
- 如果你定义的类的数据成员不是基本类型的成员而是其他的另外一个类的对象,那么这个就是类组合的情况,在这种情况下,对象成员的默认初始化方式由它所属的类来决定。
-
default 关键字
-
如果程序中定义了构造函数,默认情况下编译器就不再隐含生成默认构造函数;
-
如果此时依然希望编译器隐含生成默认构造函数,可以使用“=default”;
-
例如:在 Clock 类里面我们自己定义了一个构造函数,带三个参数,那么这个时候,编译器默认情况下就不给我们生成这个默认的构造函数了,如果我们需要的话,我们需要自己去写一个无参数的构造函数,此时我们可以用 default 来说明我们依然需要编译器隐含生成的默认构造函数,这就指示编译器依然为我们提供构造函数。
class Clock { public: Clock() = default; // 指示编译器提供默认构造函数 Clock(int newH, int newM, int newS); // 构造函数 private: int hour, minute, second; };
-
构造函数例题
-
含有自带参数的构造函数:钟表类修改
#include "pch.h" #include <iostream> using namespace std; class Clock{ public: /* 构造函数这个函数的函数名跟类名是完全一样的,包括大小写都要一样 而且这个函数没有返回值类型,也不能写成 void 类型,就是不能定义返回类型 */ Clock(int newH, int newM, int newS); // 构造函数 void setTime(int newH, int newM, int newS); void showTime(); private: int hour, minute, second; }; /* 同样实现构造函数的时候也需要把类名写在前面, 因为这个不是全局函数,是类的成员函数,它实现的时候都要写类名。 在它实现的时候呢也是不能规定返回类型, 而且这个函数体里面是不能写 return 语句的。*/ // 构造函数的实现 /* 在这个参数表之后有一个冒号,在冒号之后左大括号之前,这个序列叫初始化列表。 如果我们要对类的成员变量进行初始化的话,首选用这种方式。 如果你是做简单初始化,比如就是将形参值对应着赋值给这个类的对象的数据成员: newH 复制给 hour,newM 复制给 minute,newS 复制给 second。 那么就不必要在函数体中去写这样的简单赋值了,就将这种初始化的关系写到初始化列表里面。 它是表示用 newH 去初始化 hour 这个变量,nemM 去初始化 minute 变量,second 也是这样的。*/ Clock::Clock(int newH, int newM, int newS): // 用初始化列表的方式比在函数体中去写赋值表达式的效率要高一些 hour(newH), minute(newM), second(newS){ } // 成员函数的实现 void Clock::setTime(int newH, int newM, int newS) { } void Clock::showTime() { cout << hour << ":" << minute << ":" << second; } int main() { Clock c(8, 30, 30); // 自动调用构造函数 c.showTime(); return 0; }
-
带有默认的构造函数:钟表类修改
#include "pch.h" #include <iostream> using namespace std; class Clock { public: Clock(int newH, int newM, int newS); // 构造函数 Clock(); // 默认构造函数 void setTime(int newH, int newM, int newS); void showTime(); private: int hour, minute, second; }; // 构造函数的实现 Clock::Clock(int newH, int newM, int newS) : // 用初始化列表的方式比在函数体中去写赋值表达式的效率要高一些 hour(newH), minute(newM), second(newS) { } Clock::Clock(): // 默认构造函数 hour(0), minute(0), second(0) { } // 成员函数的实现 void Clock::setTime(int newH, int newM, int newS) { } void Clock::showTime() { cout << hour << ":" << minute << ":" << second << endl; } // 对象的使用 int main() { Clock c1(0, 0, 0); // 调用有参数的构造函数 Clock c2; // 调用无参数的构造函数 c1.showTime(); c2.showTime(); return 0; }
委托构造函数
-
类中往往有多个构造函数,只是参数表和初始化列表不同,其初始化算法都是相同的,这时,为了避免代码重复,可以使用委托构造函数。
-
委托构造函数
-
一个构造函数可以去委托另外一个构造函数来帮它完成初始化功能;
-
委托构造函数使用类的其他构造函数执行初始化过程;
/* 第二个构造函数是无参数的构造函数,它调用了另外一个有参数函数的构造函数 将默认的三个初始化参数传给有参数表的 Clock 构造函数, 这样的话,就不用把同样的初始化方法再写一遍了。*/ Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) { } Clock() : Clock(0, 0, 0) { }
-
用委托构造函数最大的好处是可以保持代码实现的一致性,如果将来想修改这个构造函数的初始化算法的时候,只要在一处修改,委托这个构造函数来实现初始化的其他构造函数的算法也就都同步修改了。
-
复制构造函数
-
我们定义对象的时候会经常用一个已经存在的对象去初始化新对象,C++ 语法为我们提供了一种特殊的构造函数叫做复制构造函数。在复制构造函数中,我们可以规定如何用一个已经存在的对象去初始化一个新对象,这个已经有了的对象可以用它的引用作为构造函数的参数。 即复制构造函数是一种特殊的构造函数,其形参为本类的对象引用,作用是用一个已存在的对象去初始化同类型的新对象。
-
隐含的复制构造函数
- 如果你在定义类的时候没有定义复制构造函数,编译器也会为我们生成一个默认的复制构造函数;
- 默认的复制构造函数会实现两个对象的数据成员之间的一 一对应复制,这样我们就不用写复制构造函数了,但是在一些时候这个默认的复制构造函数还不那么令人满意,那么这时候我们就可以利用复制构造函数这个机制自己来规定如何进行复制构造。
-
复制构造函数是一种特殊的构造函数,它的形参必须是本类对象的引用,复制构造函数的作用就是用这个形参构造新的对象。
- 在这个参数前面又要加一个 const 来限定,当我们传递引用作为参数的时候,实际上是可以双向传递数据的,也就是说,接收这个引用参数的函数,它在函数体中如果对这个形参引用做了任何修改,那么实参也会被同步修改;
- 显然这不是我们写复制构造函数的目的,我们写复制构造函数是希望用形参引用所指的那个对象去初始化新对象,但绝不希望在这个初始化过程中把原有的那个形参对象给修改了;
- 在这加一个const关键字,这就说明了这个引用是一个常引用,我们只能使用这个引用去读取它里面的数据,但是不能用这个引用对它指向的对象进行修改;
- 这样既能够传参数进来,又能够保证实参的安全性。
-
delete 关键字
-
如果不希望对象被复制构造,C++98 中可以将复制构造函数声明为 private,并且不提供函数的实现,而 C++11 中用“=delete”指示编译器不生成默认复制构造函数。
class Point { //Point 类的定义 public: Point(int xx = 0, int yy = 0) { x = xx; y = yy; } // 构造函数,内联 Point(const Point& p) = delete; // 指示编译器不生成默认复制构造函数 private: int x, y; // 私有数据 };
-
-
复制构造函数被调用的三种情况:
- 首先就是定义一个对象的时候,用本类的另外一个对象作为初始值,这个时候显然是发生了复制构造;
- 第二种情况就是如果一个函数的参数是类的对象,形参首先规定是一个类的对象,那么调用函数的时候给出的实参也应该是同类型的对象,这个时候用实参去初始化形参做形实结合的时候就发生了复制构造;
- 还有一种情况就是如果一个函数的返回值是类的对象,函数执行完了要返回主调函数的时候有一个 return 语句,return 语句中返回一个对象给主调函数,那么在这个过程中也是有可能发生复制构造的,这种情况也可以通过移动构造避免不必要的复制。
复制构造函数调用举例
-
Point 类的完整程序
#include "pch.h" #include <iostream> using namespace std; class Point { // Point 类的定义 public: Point(int xx = 0, int yy = 0) { // 首先定义了一个 Point 类 a x = xx; y = yy; } // 构造函数,内联 Point(const Point& p); // 复制构造函数 void setX(int xx) { x = xx; } void setY(int yy) { y = yy; } int getX() const { return x; } // 常函数 int getY() const { return y; } // 常函数 private: int x, y; // 私有数据 }; // 复制构造函数的实现 Point::Point(const Point& p) { x = p.x; y = p.y; cout << "Calling the copy constructor " << endl; } // 形参为 Point 类对象 void fun1(Point p) { cout << p.getX() << endl; } // 返回值为 Point 类对象 Point fun2() { Point a(1, 2); return a; } int main() { Point a(4, 5); Point b(a); // 用 a 初始化 b。 cout << b.getX() << endl; fun1(b); // 对象 b 作为 fun1 的实参 b = fun2(); // 函数的返回值是类对象 cout << b.getX() << endl; return 0; }