本文是按照(C++ Primer Plus(第6版))学习过来的。
1.类的声明与定义
类的声明与定义最好放在不同的文件中,头文件存放类的声明,实现文件存放类的定义。比如下面的例子:
//Person.h 类声明的文件
#include<string>
using namespace std;
class Person
{
public:
Person();
Person(const string& name);
~Person();
void showName();
private:
string mName;
};
//Person.cpp 类的实现文件
#include "Person.h"
#include <iostream>
Person::Person(const string& name) {
mName = name;
}
Person::Person()
{
}
Person::~Person()
{
}
void Person::showName() {
std::cout << "my name is " << mName << endl;
}
//使用类
void main(){
....
Person* p = new Person("crabisacoolboy");
p->showName();
delete p;
system("pause");
return 0;
}
2.类的构造函数和析构函数
#include<string>
using namespace std;
class Person
{
public:
//默认构造函数
Person();
//自己定义的构造函数
Person(const string& name);
//析构函数
~Person();
void showName();
private:
string mName;
};
C++的目标之一是让使用类对象就像使用标准类型一样,但是,前面定义的Person类不能够让你像初始化int或者结构那样来初始化Person对象。也就是说,常规的初始化语法不适合类型Person:
int year =2018; //valid initialization
struct thing{
char *pn;
int m;
};
thing amabob= {"wodget",-23}; //valid initialization
Person people = {"xie"}; //NO! compile error
不能够像上面初始化Person对象的原因在于,数据部分的访问状态是私有的,这意味着程序不能够直接访问数据成员。我们是通过成员函数来访问数据成员的,因此需要设计合适的成员函数,才能够成功地对对象初始化(如果使数据成员成为公有,而不是私有,就可以按照刚才初始化类对象的方法初始化,但使数据成为公有的违背了类的一个主要初衷:数据隐藏。
构造函数的原型和函数头有一个有趣的特征:虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有类型声明。C++提供了两种使用构造函数的方式:
//第一种方式,显示的调用构造函数
Person p1 = Person("xie");
//第二种方式,隐示的调用构造函数
Person p2("xie");
这种格式更加紧凑,它与上面第一显示调用等价
每次创建类对象(甚至使用new动态内存分配)时,C++都使用构造函数。下面是将构造函数与new一起使用的方法:
Person* p=new Person("xie");
这条语句创建了一个Person对象,将其初始化为参数提供的初始值,并将该对象的地址赋给p指针,在这种情况下,对象没有名称,但可以使用指针来管理该对象
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏:析构函数。析构函数完成清理工作,因此实际很有用。例如,如果构函数使用new来分配内存,则析构函数使用delete来释放内存。
请看下面代码:
const Person p2 = Person("xie");
p2.showName();
对于当前的C++来说,编译器拒绝编译第二行。编译器会发出如下错误:
error C2662: “void Person::showName(void)”: 不能将“this”指针从“const Person”转换为“Person &”
这是什么原因呢?因为show的代码无法确保调用对象不被修改:调用对象和const一样,不应被修改。我们以前通过将函数参数声明为const引用或指向const的指针来解决这种问题。但这里存在语法问题,这里showName()没有参数。相反,它所使用的对象是由方法调用隐示提供的。需要一种新的语法,保证函数调用不会修改对象,C++的解决方法是将const关键字放在函数的括号后面。也就是说,showName()声明与定义应该像这样:
//声明
void showName() const;
//定义
void Person::showName() const{
...
}
3.友元函数
(1)为什么需要友元函数呢?
在为类重载二元运算符时(带两个参数的运算符)常常需要友元。将Time对象乘以实数就属于这种情况。在Time类中,重载的乘法运算符与其它两种重载的运算符(比如加法、减法)的差别在于,它使用了两种不同的类型。也就是说,加法和减法运算符都结合两个Time值,而乘法运算符将一个Time值与一个double值结合在一起。这限制了该运算符使用的方式。记住,左侧的操作数是调用对象。也就是说,下面的语句:
A = B * 2.75;
将被转为下面的函数调用:
A = B.operator*(2.75)
但下面语句又如何呢?
A = 2.75 * B; //can't correspond to a member function
从概念上说,2.75*B应与B*2.75相同,但第一个表达式不对应任何成员函数,因为2.75不是Time类型对象。记住左侧的操作数应是调用对象,但2.75不是对象。因此编译器不能够使用成员函数调用来替换该表达式。解决这个难题的一种方式是使用非成员函数(记住大多数的运算符都可以通过成员或者非成员函数来重载)。非成员函数不是由对象调用的,它所使用的值都是显示参数。这样编译器能够将下面的表达式:
A = 2.75 * B;
将与下面的非成员函数调用匹配:
A = operator*(2.75,B);
该函数的原型如下:
Time operator*(double m,const Time& t);
使用非成员函数可以按所需的顺序来提供操作数(先是double,然后是Time),但引发了一个新的问题:非成员函数不能够直接访问类的私有数据,至少常规非成员函数不能够访问。然而有一类特殊的非成员函数可以访问类的私有成员,它们被称作友元函数。
(2)友元函数声明和定义
创建友元函数的第一步是将其原型放在类的声明中,并在原型声明前加上关键字friend:
friend Time operator*(double m,const Time& t); //goes in class declaration
该原型意味着以下两点:
I:虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能够使用成员运算符来调用。
II:虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
第二步是编写函数定义,因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend,定义应该如下:
Time operator*(double m,const Time& t){ //friend not used in definition
Time result;
long totalminutes = t.hours * m * 60 + t.minutes * m;
....
return result;
}
有了以上声明和定义后,下面的语句
A = 2.75 * B;
将转换为如下语句,从而调用刚才定义的非成员友元函数:
A = operator*(2.75,B);
总之,类的友元函数是非成员函数,其访问权限与成员函数相同。
4.类的静态成员
(1)类静态成员声明
class Home{
public:
Home();
~Home();
private:
static int sTelephoneNumber; //静态成员
};
(2)静态类成员定义
int Home::sTelephoneNumber = 123;
这条语句将类的静态成员sTelephoneNumber的值初始化为123。请注意,不能够在类的声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态成员是单独存储的,而不是对象的组成部分。请注意,初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static.初始化是在类的方法文件,而不是在类的声明文件中进行的。这是因为类的声明位于头文件,程序可能将头文件包含在其它几个文件中。如果在头文件中进行初始化,将出现多个初始化语句的副本,从而引发错误。
注意:静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整型或者枚举型const,则可以在类声明中初始化。
5.特殊成员函数
C++自动提供了下面这些成员函数:
- 默认构造函数,如果没有定义构造函数
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符,如果没有定义
更准确的说,编译器将生成上述最后三个函数的定义,如果程序使用对象的方式要求这么做。例如,如果你将一个对象赋给另一个对象,编译器将提供赋值运算符的定义。
(1)默认构造函数
如果没有提供任何构造函数,C++将创建默认构造函数。例如定义一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:
Klunk::Klunk(){ //implicit default constructor
}
也就是说,编译器将提供一个不接受任何参数,也不执行任何操作的构造函数(默认构造函数),这是因为创建对象总会调用构造函数:
Klunk klunk;//invokes default constructor
默认构造函数使Klunk类似于一个自动变量,也就是说,它的值在初始化时是未知的。
如果定义了构造函数,C++将不会定义默认的构造函数。如果希望在创建对象时不显示地对它进行初始化,则必须显示的定义默认构造函数。这种构造函数没有任何参数,但可以用来设定特定的值:
Klunk::Klunk(){ //explicit default constructor
klunk_ct = 0;
}
带参数的构造函数也可以是默认构造函数,只是所有的参数都由默认值。例如Klunk类可以包含下述构造函数:
Klunk::Klunk(int n=0){
klunk_ct = n;
}
但只能够有一个默认的构造函数,也就是说,不能够下面这样做:
Klunk::Klunk(){ //constructor#1
klunk_ct = 0;
}
Klunk::Klunk(int n=0){ //ambiguous constructor#2
klunk_ct = n;
}
这为何会有二义性呢?请看下面两个声明:
Klunk kar(10);//clearly matches Klunk(int n);
Klunk bus; //could match either constructor
第二个声明既与构造函数#1(没有参数),也与构造函数#2(使用默认参数0)匹配。这将导致编译器发出一条错误信息。
(2)复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
Class_name(const Class_name&);
它接受一个指向类对象的常量引用作为参数。例如,Klunk类的复制构造函数的原型如下:
Klunk(const Klunk&);
对于复制构造函数,需要知道两点:何时调用和有何功能。
I:何时调用复制构造函数?
新创建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显示的初始化为现有的对象。例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数:
StringBad ditto(motto); //calls StringBad(const StringBad&);
StringBad metto=motto; //calls StringBad(const StringBad&);
StringBad also=StringBad(motto); //calls StringBad(const StringBad&);
StringBad *p=new StringBad(motto); //calls StringBad(const StringBad&);
其中中间的2种声明可能会使用复制构造函数直接创建对象metto和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metto和also,这取决于具体的实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给指针p。每当程序生成了对象的副本,编译器都将使用复制构造函数。具体的说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。记住,按值传递意味着创建原始变量的副本。编译器生成临时对象时,也将使用复制构造函数。由于按值传递对象将使用复制构造函数,因此应该按引用传递对象。这样可以节省调用复制构造函数的时间以及存储新对象的空间。
II:复制构造函数的功能
默认的复制构造函数逐个复制非静态成员(成员复制也称浅复制),复制的是成员的值。如果成员本身是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员函数和静态成员变量不受影响,因为它们属于整个类,而不是具体的某个对象。解决类设计中这种问题的方法是进行深度复制(deep copy)。也就是说,复制构造函数应当复制字符串并将其副本地址赋给str成员,而不是仅仅复制字符串地址。这样每个对象都有自己的字符串,而不是引用另外一个对象的字符串。调用析构函数都将释放不同的字符串,而不会试图去释放一个已经释放的字符串。必需显示定义复制构造函数的原因在于,一些类成员使用new初始化的、指向数据的指针,而不是数据本身。
警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。复制的另外一种形式(成员复制或浅复制)都是复制指针值。浅复制仅浅浅的复制指针的信息,而不会深入"挖掘"以复制指针引用的结构。
(3)赋值运算符
赋值运算符的原型如下:
Class_name& Class_name::operator=(const Class_name&);
例如StringBad类的赋值运算符的原型如下:
StringBad& StringBad::operator=(StringBad&);
初始化对象时并不一定会使用赋值运算符:
StringBad metto = knot; //use copy constructor,possibly assignment,too
这里,metto是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。然而,正如前面所说,实现时也可能分两步来处理这条语句。使用复制构造函数来创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=运算符时也可能调用赋值运算符。与复制构造函数类似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身是类对象,则程序使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。自定义赋值运算符应注意下面几点:
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete来释放这些数据
- 函数应当避免将对象赋给自身:否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
- 函数返回一个指向调用对象的引用。
例如StringBad的赋值运算符:
if(this==&st){
return *this;
}
delete[] str;
...
return * this;
}
6.静态成员函数
可以将成员函数声明为静态的(函数声明必需包含关键字static,但如果函数定义是独立的,则其中不能够包含关键字static),这样做有两个重要的后果:首先不能够通过对象调用成员函数,实际上,静态成员函数甚至不能够使用this指针。如果静态成员函数是在公有部分声明,则可以使用类名和作用域解析运算符来调用它。其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态成员。
例如,可以给StringBad类添加一个howMany()的静态成员函数,方法是在类中添加如下原型/定义:
static int howMany(){
return num_strings;
}
调用它的方式如下:
int count = StringBad::howMany();
7.有关返回对象的说明
(1)返回指向const对象的引用
使用const引用的常见原因在于提高效率,但对于何时可以采用这一种方式存在一些限制。如果函数返回(通过调用对象的方法或者将对象作为参数)传递给它的对象,可以通过返回引用来提高效率。例如,编写函数max(),它返回两个Vector对象中较大的一个,该函数以下面的方式被使用:
Vector force1(50,60);
Vector force2(10,70);
Vector maxVector;
maxVector = max(force1,force2);
下面两种实现都是可行的:
//version1
Vector max(const Vector& v1,const Vector& v2){
if(v1.magval()>v2.magval()){
return v1;
}else{
return v2;
}
}
//version2
const Vector& max(const Vector& v1,const Vector& v2){
if(v1.magval()>v2.magval()){
return v1;
}else{
return v2;
}
}
这里有3点需要说明下。首先,返回对象将调用复制构造函数,而返回引用不会。因此,第二个版本所做的工作少些,效率更高。其次,引用指向的对象应该在调用函数执行时存在。在这个例子中,引用指向force1或force2,它们都是在调用函数中定义的,因此满足条件。第三,v1和v2都被声明为const引用,因此返回类型必需为const,这样才匹配。
(2)返回对象
如果返回的对象是被调用函数中的局部变量,则不应该按引用的方式来返回它,因为在被调用函数执行完毕时,局部对象将会调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象时不存在的。在这种情况下,返回对象而不是引用。通常被重载的算术运算符属于这一类。例如:
Vector force1(50,60);
Vector force2(10,70);
Vector net;
net = force1+force2;
返回的不是force1,也不是force2,force1和force2在这个过程应该保持不变。因此,返回值不能够是指向在调用函数中已经存在的对象。相反,在Vector::operator+()中计算得到的两个矢量的和被存在一个新的临时对象中,该函数也不应该返回指向该临时对象的引用,而应该是返回实际的Verctor对象,而不是引用:
Vector Vector::operator+(const Vector& b){
return Vector(x+b.x,y+b.y);
}
在这种情况下,存在调用复制构造函数来创建被返回的对象的开销,然而这是无法避免的。在上面实例中,构造函数创建了一个方法operator+()能够返回的对象,而返回语句引发的复制构造函数的隐式调用创建一个调用程序能够访问的对象。总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或者函数返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法或者函数可以返回对象,也可以返回对象的引用,应首先选引用,这样效率更高。
8.使用指向对象的指针
C++程序经常使用指向对象的指针。例如:Person* p =new Person("xie");这里指针p指向new创建的未命名对象。
使用new初始化对象
通常,如果Class_name是类,value的类型为Type_name,则下面语句:
Class_name* pClass = new Class_name(value);
将调用如下构造函数:
Class_name(Type_name);
这里可能还有一些琐碎的转换,例如:
Class_name(const Type_name&);
另外,如果不存在二义性,则将发生由原型匹配导致的转换(如int到double)。下面的初始化方式将调用默认的构造函数:
Class_name* pClass = new Class_name;
使用对象指针时,要注意以下几点:
- 使用常规表示法来声明指向对象的指针:String* pStr;
- 可以将指针初始化为指向已有的对象:String* first=&sayings[0];
- 可以使用new来初始化指针,这将创建一个新的对象。String* p=new String(sayings[0]);
- 对类使用new将会调用相应的构造函数来初始化新建的对象。
- 可以使用->运算符通过指针访问类方法。
- 可以对对象指针应用解除引用运算符(*)来获得对象。