类 1(萌新学习==)

1 类的定义

1.1 形式定义形式

class 类名{
    private:
        私有数据成员和私有成员函数;//统称私有成员
    public:
        公有数据成员和公有成员函数;//统称公有成员
};
  • private和public出现次数任意,顺序也可随意,如果没有指明访问特性,该成员默认为private。
  • 私有成员只能被同一个类的成员函数访问,不能被全局函数或其它类的成员函数访问。公有成员可以通过变量名.成员名被程序中的其他所有函数访问

1.2 数据成员

数据成员可以是C++基本数据类型,也可以是构造数据类型(如结构体和类),如:

class Team;     //已定义的类
class Grade
{
    Tream a;    //已定义的类
    Grade *p;   //指向正在定义的类的指针
    Grade &r;   //引用
    Grade b;    //错误,使用未完整定义的类定义数据成员
}:
  • C++11标准允许类在定义时初始化数据成员

1.3 函数成员

函数成员的定义、声明格式与非成员函数(全局函数)相同。成员函数的函数的定义可以放在类中定义,也可以放在类外。
类中定义的成员函数被称为内联函数(一般是比较简单的函数),如果在类外定义成员函数必须同时在类内声明成员函数的原型。类外定义函数的格式如下:

返回值类型 类名::成员函数名(行参表)
{
    函数体;
}
  • ::是类的作用于分辨符,表明后面的成员函数属于前面那个类
  • 类外定义函数必须要在类定义完整之后(就是不能写在对应类定义的上面)
    如:
#include<iostream>
using namespace std;

class Clock
{
    private:
        int a=1,b=2;
        int times(int x,int y)
        {
            return x*y;
        }
    public:
        int c=3;
        void pt(int);
};

void Clock::pt(int d)
{
    cout<<a+b+c+d<<endl;//如果形参名和数据成员名相同,则会采用形参的值
    cout<<times(b,d);
}
int main()
{
    Clock aa;
    aa.pt(2);
    return 0;
}

输出结果为

8
4

关于带默认形参的成员函数:
类外定义函数体,形参的缺省值可以在函数原型中指定,也可以在类外定义函数体时指定(这一点上和全局函数有些区别),但不能两个地方重复指定。原型声明中可以省略形参,省略形参后同样可以带缺省值。不过定义不能省略形参。一般都采用原型中带默认形参的方式。
重载成员函数:
同样成员函数也可重载,规则和全局函数基本一致。

2 对象的使用

用类定义的变量叫做对象

2.1定义

类名+对象名; 如:
一般Clock MyClock; 数组Clock MyClock[NUM]; 指针Clock *MyClock; 引用Clock &MyClock; 动态变量Clock MyClock 动态数组Clock MyClock[NUM];

2.2 访问

2.2.1 函数

  • 对于函数公有直接用,私有通过公有函数调用,上面有过举例。
  • 成员函数可以使用本类的其它对象的私有成员

2.2.2 对象指针(对象数组和对象大概不用讲罢,留着到构造函数去了)

初始化

Clock aa;
Clock *q=&aa;

访问对象方式

q->数据函数名 或 成员函数名(参数表);
(*q).数据函数名 或 成员函数名(参数表);
例如:
q->pt(2);
(*q).c;

同样不能访问私有的。
This指针:
对于类Clock,如果我们定义了多个同一类型的对象,但我们调用函数pt时,我们怎么确保里面的a,b,c是我们调用成员函数的对象里的a,b,c?实际上,C++为每个成员函数设置了一个隐藏的指向本类型的指针形参this,它指向当前被调用成员函数的对象。

class Clock
{
    private:
        int a=1,b=2;
        int times(int x,int y)
        {
            return x*y;
        }
    public:
        int c=3;
        void pt(int);
};

void Clock::pt(int d)
{
    cout<<a+b+c+d<<endl;
    cout<<times(b,d);
}

对于pt,它的定义如下:

void Clock::pt(int d)
{
    cout<<a+b+c+d<<endl;
    cout<<times(b,d);
}

但实际形式为

void Clock::pt(int d)
{
    cout<<  this->a  +  this->b  +  this->c  +d<<endl;
    cout<<times(  this->b  ,d);
}

通常,在写成员函数时可以省略this,编译时会自动加上。但如果成员函数中要把对象作为整体访问时,必须显式地使用指针this,即*this。

2.2.3 对象引用

类名 &对象引用名=被引用对象;

  • 对象引用和被引用对象必须同一类型
  • 除非作为函数参数的与函数返回值,对象引用必须定义时初始化
  • 定义引用是没有定义一个对象,不没有分配空间,不调用构造函数(构造函数在下面)
    其他操作基本就那样了,优点是作为函数形参的时候。

2.2.4 动态对象和动态对象数组

对象:对象指针=new 类名; 对象数组:对象指针=new 类名[NUM];

  • 注意指针必须是指向该类名的对象哦~!
    其它的操作就和对象指针差不多了,但是由于是动态的,退出函数不会消亡,一直存在堆区,所以要用delete。如:
delete 对象指针;
delete [] 对象指针;

初始化同样留到下面讲。

3 对象的构造与析构

3.1 对象的构造

3.1.1 构造函数

构造函数用于对对象赋初值。性质如下:

  • 名字与类名相同
  • 不能指定返回值类型,void也不行。
  • 不能直接调用
  • 定义一个类时,如果用户没有定义构造函数,编译器去会提供一个默认的构造函数,而成员函数不存在这一点。
  • 对象可能会有多个数据成员,所以可能赋初值的个数不尽相同,因此构造函数可以重载,来满足这方面的需求。
    例如,可以为Clock添加带有默认形参的构造函数(一般在公有区,私有区就用不了对吧)
Clock(int n1=0,int n2=0,int n3=0)
{
    a=n1;
    b=n2;
    c=n3;
}

如果在类外:

Clock::Clock(int n1,int n2,int n3)//缺省值在声明里
{
    a=n1;
    b=n2;
    c=n3;
}

有了这个函数,就可以把函数定义和初始化工作一起完成。
对于Clock类,如果只有上面一个构造函数,可以:Clock aa(10,20)Clock aa,有于默认了形参,前者的成员c采用默认值,后者a,b,c用的都是默认值。
如果只定义了以下一个构造函数

Clock(int n1,int n2,int n3)
{
    a=n1;
    b=n2;
    c=n3;
}

则初始化只能是Clock aa(NUM1,NUM2,NUM3);,哪怕不初始化也不行,如Clock aa;就会报错。
于是就可以通过带默认形参和函数重载解决。
其他:

  • 如果没有写构造函数,则编译器会提供一个没有形参的构造函数,这时候定义一个对象里面的值均为随机值(如果你没有给数据成员写默认值)。
  • 通过构造函数初始化只能在定义中完成。
  • 尽管你可以像结构体一样不写构造函数只接有大括号进行赋值,但这种在你有私有数据成员的情况下可能会报错。

对象数组初始化方式

Clock aa[NUM]={{NUM1,NUM2,NUM3},{NUM1,NUM2,NUM3},{...},...};
Clock aa[NUM]={Clock(NUM1,NUM2,NUM3),Clock(NUM1,NUM2,NUM3),Clock(...)...};

两种方法均等价且可以混用,都调用了NUM次构造函数。
动态对象和动态对象数组初始化

Clock *q=new Clock(NUM1,NUM2,NUM3);//和一般对象相同
Clock *q=new Clock[NUM] {{NUM1,NUM2,NUM3},{NUM1,NUM2,NUM3},{...},...};//和一般对象数组相同,不过没有等号

3.1.2 构造函数的初始化列表

构造函数有一个与普通函数不一样的地方,它可以包含一个构造函数的初始化列表,其位于函数头和函数体之间,如:

Clock(int m1,int m2,int m3):a(m1),b(m2),c(m3)
{
    d=a+b+c;
}

作用就是实参的值传递给m1,m2,m3后,在通过初始化列表赋给a,b,c,然后只接采用a,b,c给d赋值。(可能是这样?看书时没太看懂)
它有以下优点

  • 提高构造函数效率
  • 有些场合必须应用初始化列表,比如数据成员是对象的时候可能无法只接传递给形参和当数据成员是常量的时候(后面有)。

3.2.3 复制构造函数

资料来源自:http://c.biancheng.net/view/151.html

复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。

复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。

如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。

注意,默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。

下面是一个复制构造函数的例子。

#include<iostream >
using namespace std;
class Complex
{
public:
    double real, imag;
    Complex(double r, double i) {
        real= r; imag = i;
    }
};
int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //用复制构造函数初始化c2
    cout<<c2.real<<","<<c2.imag;  //输出 1,2
    return 0;
}

第 13 行给出了初始化 c2 的参数,即 c1。只有编译器自动生成的那个默认复制构造函数的参数才能和 c1 匹配,因此,c2 就是以 c1 为参数,调用默认复制构造函数进行初始化的。初始化的结果是 c2 成为 c1 的复制品,即 c2 和 c1 每个成员变量的值都相等。

如果编写了复制构造函数,则默认复制构造函数就不存在了。下面是一个非默认复制构造函数的例子。

#include<iostream>
using namespace std;
class Complex{
public:
    double real, imag;
    Complex(double r,double i){
        real = r; imag = i;
    }
    Complex(const Complex & c){
        real = c.real; imag = c.imag;
        cout<<"Copy Constructor called"<<endl ;
    }
};
int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //调用复制构造函数
    cout<<c2.real<<","<<c2.imag;
    return 0;
}

程序的输出结果是:

Copy Constructor called
1,2

第 9 行,复制构造函数的参数加不加 const 对本程序來说都一样。但加上 const 是更好的做法,这样复制构造函数才能接受常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。

第 17 行,就是以 c1 为参数调用第 9 行的那个复制构造函数初始化的。该复制构造函数执行的结果是使 c2 和 c1 相等,此外还输出Copy Constructor called。

可以想象,如果将第 10 行删去或改成real = 2*c.real; imag = imag + 1;,那么 c2 的值就不会等于 c1 了。也就是说,自己编写的复制构造函数并不一定要做复制的工作(如果只做复制工作,那么使用编译器自动生成的默认复制构造函数就行了)。但从习惯上来讲,复制构造函数还是应该完成类似于复制的工作为好,在此基础上还可以根据需要做些別的操作。

构造函数不能以本类的对象作为唯一参数,以免和复制构造函数相混淆。例如,不能写如下构造函数:

Complex (Complex c) {...}

复制构造函数被调用的三种情况
复制构造函数在以下三种情况下会被调用。

  1. 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。
Complex c2(c1);
Complex c2 = c1;

这两条语句是等价的。

注意,第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:

Complex c1, c2; c1 = c2 ;
c1=c2;

这条语句不会引发复制构造函数的调用,因为 c1 早已生成,已经初始化过了。

  1. 如果函数 F 的参数是类 A 的对象,那么当 F 被调用时,类 A 的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
#include<iostream>
using namespace std;
class A{
public:
    A(){};
    A(A & a){
        cout<<"Copy constructor called"<<endl;
    }
};
void Func(A a){ }
int main(){
    A a;
    Func(a);
    return 0;
}

程序的输出结果为:

Copy constructor called

这是因为 Func 函数的形参 a 在初始化时调用了复制构造函数。

前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Func 函数的形参 a 的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。

以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。

如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用。例如:

void Function(const Complex & c)
{
    ...
}

这样,Function 函数中出现任何有可能导致 c 的值被修改的语句,都会引发编译错误。

思考题:在上面的 Function 函数中,除了赋值语句,还有什么语句有可能改变 c 的值?例如,是否允许通过 c 调用 Complex 的成员函数?

  1. 如果函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:
#include<iostream>
using namespace std;
class A {
public:
    int v;
    A(int n) { v = n; };
    A(const A & a) {
        v = a.v;
        cout << "Copy constructor called" << endl;
    }
};
A Func() {
    A a(4);
    return a;
}
int main() {
    cout << Func().v << endl;
    return 0;
}

程序的输出结果是

Copy constructor called
4

第19行调用了 Func 函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用复制构造函数时,实参就是第 16 行 return 语句所返回的 a。复制构造函数在第 9 行确实完成了复制的工作,所以第 19 行 Func 函数的返回值和第 14 行的 a 相等。

需要说明的是,有些编译器出于程序执行效率的考虑,编译的时候进行了优化,函数返回值对象就不用复制构造函数初始化了,这并不符合 C++ 的标准。上面的程序,用 Visual Studio 2010 编译后的输出结果如上所述,但是在 Dev C++ 4.9 中不会调用复制构造函数。把第 14 行的 a 变成全局变量,才会调用复制构造函数。对这一点,读者不必深究。

3.2 对象的析构

资料来源:http://c.biancheng.net/view/152.html 析构函数(destructor)是成员函数的一种,它的名字与类名相同,但前面要加~,没有参数和返回值。

一个类有且仅有一个析构函数。如果定义类时没写析构函数,则编译器生成默认析构函数。如果定义了析构函数,则编译器不生成默认析构函数。

析构函数在对象消亡时即自动被调用。可以定义析构函数在对象消亡前做善后工作。例如,对象如果在生存期间用 new 运算符动态分配了内存,则在各处写 delete 语句以确保程序的每条执行路径都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用 delete 语句,就能确保对象运行中用 new 运算符分配的空间在对象消亡时被释放。如果对象本身是动态对象,仍然需要手动delte去消亡,当然,delete同时也会自动调用析构函数来释放对象内部使用new分配的空间。
例如下面的程序:

class String{
private:
    char* p;
public:
    String(int n);
    ~String();
};
String::~String(){
    delete[] p;
}
String::String(int n){
    p = new char[n];
}

String 类的成员变量 p 指向动态分配的一片存储空间,用于存放字符串。动态内存分配在构造函数中进行,而空间的释放在析构函数 ~String() 中进行。这样,在其他地方就不用考虑释放空间的事情了。

只要对象消亡,就会引发析构函数的调用。下面的程序说明了析构函数起作用的一些情况。

#include<iostream>
using namespace std;
class CDemo {
public:
    ~CDemo() {  //析构函数
        cout << "Destructor called"<<endl;
    }
};
int main() {
    CDemo array[2];  //构造函数调用2次
    CDemo* pTest = new CDemo;  //构造函数调用
    delete pTest;  //析构函数调用
    cout << "-----------------------" << endl;
    pTest = new CDemo[2];  //构造函数调用2次
    delete[] pTest;  //析构函数调用2次
    cout << "Main ends." << endl;
    return 0;
}
程序的输出结果是:
Destructor called
-----------------------
Destructor called
Destructor called
Main ends.
Destructor called
Destructor called

第一次析构函数调用发生在第 13 行,delete 语句使得第 12 行动态分配的 CDemo 对象消亡。

接下来的两次析构函数调用发生在第 16 行,delete 语句释放了第 15 行动态分配的数组,那个数组中有两个 CDemo 对象消亡。最后两次析构函数调用发生在 main 函数结束时,因第 11 行的局部数组变量 array 中的两个元素消亡而引发。

函数的参数对象以及作为函数返回值的对象,在消亡时也会引发析构函数调用。例如:

#include <iostream>
using namespace std;
class CDemo {
public:
    ~CDemo() { cout << "destructor" << endl; }
};
void Func(CDemo obj) {
    cout << "func" << endl;
}
CDemo d1;
CDemo Test() {
    cout << "test" << endl;
    return d1;
}
int main() {
    CDemo d2;
    Func(d2);
    Test();
    cout << "after test" << endl;
    return 0;
}
程序的输出结果是:
func
destructor
test
destructor
after test
destructor
destructor

程序共输出 destructor 四次:
第一次是由于 Func 函数结束时,参数对象 obj 消亡导致的。
第二次是因为:第 20 行调用 Test 函数,Test 函数的返回值是一个临时对象,该临时对象在函数调用所在的语句结束时就消亡了,因此引发析构函数调用。
第三次是 main 函数结束时 d2 消亡导致的。
第四次是整个程序结束时全局对象 d1 消亡导致的。

  • 个人有一个不太明白的地方,如果定义一个动态对象,过程中没有使用delete,但程序结束的时候没有自动调用构析函数,这是为什么。