# 四. 类和对象

# 1.定义类的一般形式

class 类名

{

public:

<公有数据和函数>

protect:

<保护数据和函数>

private:

<私有数据和函数>

};

  • 成员函数的实现,可以放在类体内;也可以放在类体外。在类体外时,必须在类体内给出原型声明

  • 放在类体内的函数被默认为内联函数,而放在类体外定义的函数时一般函数,如果定义为内联函数,则需在前面加上inline

  • 在类体定义成员函数(一般用于复杂函数)的一般形式为:

    <返回类型> <u><类名>::</u><成员函数名>(<参数说明>)

    {函数体}

访问权限

private:不能直接访问

protect:不能通过对象访问

public:可以访问

# 2.对象

  • 对象是类的实例或实体,一般格式为:<类名> <对象名表>;

  • 对象成员的访问:

  1. 圆点访问方式

    对象名.成员名 或 *(指向对象的指针).成员名

  2. 指针访问方式

    对象指针变量名->成员名(&对象名)->成员名

Point p1;//定义一个点类对象
Point *point_p1;//定义点类指针
point_p1=&p1;

p1.set(2,2);//set为类内函数,以圆点访问方式访问类内函数
point_p1->show();//以指针访问方式访问类内函数
(*point_p1).set1(3,3);
(&p1)->show();
1
2
3
4
5
6
7
8

# 3.类的界面和实现

为了减少代码的重复,加快编译速度,在大型程序设计中,C++的类结构常分为两部分:类的界面类的实现

如:

类的界面:

class Point//类的界面,一般放入.h文件中
{
    private:
    int x,y;//数据成员
    public:
    void set1(int a,int b);//成员函数
    void show();//成员函数
};
1
2
3
4
5
6
7
8

类的实现:

void POint::set1(int a,int b)//类的实现,一般在.cpp文件中
{
    x=a; y=b;
}
void Point::show()
{
    cout<<"("<<x<<","<<y<<")"<<endl;
}

//主函数
int main()
{
    Point p1;
    p1.set(1,1);
    p1.show();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.构造函数

构造函数是一种特殊的成员函数,对象的创建和初始化由它完成

  • 格式:<类名>::<类名>(<形参表>) //前面的 <类名>:: 是当构造函数定义在类外时写的

{<函数体>}

  • 特点:
  1. 被声明为公有
  2. 函数名与类名相同
  3. 可以重载
  4. 不能指定返回类型
  5. 不能被显示调用(是自动调用的)

# (1)默认构造函数

默认构造函数就是无参数的构造函数。既可以是自己定义的,也可以是编译系统自动生成的

当没有为一个类定义任何构造函数的情况下,编译系统就会自动生成一个无参数、空函数体的默认构造函数

格式:<类名>::<类名>( )

{ }

# (2)具有默认参数的构造函数

如果构造函数的参数值通常是不变的,只有在特殊情况下才需要改变它的值,这时可以将其定义为带默认参数的构造函数

如:Point (int x=0,int y=0){...}

# (3)成员初始化列表

带有成员初始化列表的构造函数一般形式:

<类名>::<构造函数名>(<参数表>):<成员初始化表>

{构造函数体}

成员初始化列表的一般形式:

数据成员名1(初始值1),数据成员名2(初始值2), … …

如:

class Sample
{
    int x;
    int &rx;//类里不能出现赋值语句,const也只能用初始化表
    public:
    Sample(int x1):x(x1),rx(x)//与在函数体内写x=x1;rx=x;等价
    {...}
    //即:
    //Sample(int x1)
    //{x=x1;rx=x;}
    void print()
    {cout<<"x="<<x<<"rx="<<rx;}
};

int main()
{...}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# (4)析构函数

  • 特点:
  1. 只能被声明为公有函数
  2. 析构函数的名字同类名,与构造函数的区别在于析构函数名前加~,表明它的功能与构造函数功能相反
  3. 析构函数没有参数,不能重载一个类中只能定义一个析构函数
  4. 不能指定返回类型
  5. 析构函数在释放一个对象时自动调用
  6. 析构函数调用顺序与构造函数调用顺序相反
  • 默认析构函数

如果一个类中没有定义析构函数时,系统将自动生成一个默认析构函数

格式:~<类名>()

{ }

# (5)拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它的作用是用一个已经存在的对象去初始化另一个对象

  • 格式:<类名>::<类名>(const<类名>&<对象名>) //const的作用是保护传过来的对象不被改变

{<函数体>}

  • 特点:
  1. 拷贝构造函数名字与类名相同,不能指定返回类型
  2. 拷贝构造函数只有一个参数该参数是该类的对象的引用
  3. 它不能被显示调用
  • 在以下条件下 会被自动调用
  1. 当用类的对象去初始化另一个对象时

    如:Point p2(p1); //Point为类名,代入法

    Point p3=p1; //赋值法

  2. 当函数的形参是类的对象,进行形参和实参的结合时

    如:

    fun1(Point p)//会调用
    { p.print(); }
    
    int main()
    {
        Point p1(10,20);
        fun1(p1);//当调用函数,进行形参与实参的结合时
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  3. 当函数的返回值是类的对象,函数执行完成返回调用者时

如:

Point fun2()
{
    Point p1(10,30);
    return p1;//会调用
}

int main()
{
    Point p2;
    p2=fun2();//函数执行完成,返回调用时
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 浅拷贝和深拷贝

默认的拷贝构造函数:如果一个类中没有定义拷贝构造函数,则系统自动生成一个默认的拷贝构造函数

如:Point (const Point &p)

{ x=p.x; y=p.y; }

浅拷贝默认的拷贝构造函数,实现数据成员逐一赋值

深拷贝:需要自己写的拷贝构造函数,实现额外的内容(如含有指针变量)

# (6)赋值运算符函数

其作用与拷贝构造函数类似,都是用一个已经存在的对象去初始化另一个对象

有点像运算符重载

  • 格式:<类名>& operator = (const <类名> &<对象名>) //其形参与拷贝构造函数一样

{

… …

return *this;

}

  • 与拷贝构造函数的不同
    1. 拷贝构造函数是定义的同时赋值;赋值运算符函数是先定义,而后再赋值
    2. 返回this指针

# (7)转移赋值运算符函数

  • 防止自赋值

    例:

    #include <iostream>
    #include <algorithm> //包含copy()函数
    using namespace std;
    class Myclass
    {
        private:
        int* data;
        size_t size;
        public:
        Myclass(size_t size):size(size),data(new int[size])//构造函数,初始化对象
        {
            for(size_t i = 0;i < size;i++)
            {
                data[i] = i;
            }
        }
        
        ~Myclass()//析构函数,清理资源
        {
            delete[] data;
        }
        
        Myclass& operator=(const Myclass& other)//重载赋值运算符函数
        {
            if(this == &other)//防止自赋值
            {
                return *this;
            }
            
            delete[] data;//释放当前对象的资源
            
            //分配新的资源并复制数据
            size = other.size;
            data = new int[size];
            copy(other.data, other.data + size, data);//这是一个更安全、更通用的做法,可以避免潜在的索引越界问题。当然也可以用下面的循环进行复制
            /*for(size_t i = 0; i < size; i++)
            {
                data[i] = other.data[i];
            }*/
            return *this;
        }
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
  • 防止自复制

    待完善。。。

# (8)转换构造函数

转换构造函数是指能够把参数类型的数据转换为类对象的构造函数

在调用时,需提供一个实参

两种形式:

  1. 构造函数本身只带有一个参数,函数原型<类名> (<不同于类的其他类型>)
  2. 构造函数带有若干个参数,但是除最左边的参数外,其他参数都有默认值,函数原型:<类名>(<类型1,类型2 = 值1,… >)

**注意:**上面两种函数不能同时出现在一个类中,否则会引发匹配构造函数是的二义性

如:

#include<iostream>
class RMB
{
    private:
    int yuan,jiao,fen;
    public:
    RMB(int y,int j,int f):yuan(y),jiao(j),fen(f){}
    
    RMB(double d = 0)//转换构造函数
    {
        int n = int(d * 100);//保留小数点后两位
        yuan = n / 100;
        jiao = (n - 100 * yuan) / 10;
        fen = n % 10;
    }
    void Print()
    {
        cout<<yuan<<"元"<<jiao<<"角"<<fen<<"分"<<endl;
    }
};

int main()
{
    RMB r1(10,5,8);//构造对象
    r1.Print();//输出10元5角8分
    RMB r2(19.86);//转换构造对象
    r2.Print();//输出19元8角6分
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 通常发生数据类型隐式转换的4种场合:
    • 算数混合运算、赋值运算、实参传递给形参、函数返回值
RMB r;
r = 19.86;//赋值运算时的类型转换,double型转换为RMB型

void f(RMB r){...}
f(19.86);//参数传递时

RMB g(double d)
{ return d; }//函数返回时
g(19.86);
1
2
3
4
5
6
7
8
9

# explicit关键字

转换构造函数能够把参数类型的数据隐式转换为类的对象,这种转换有时可能不太受欢迎。

关键字explicit可以抑制转换构造函数的隐式类型转换的做法

只要把该关键字放在转换构造函数的前面,声明该函数为显示类型的构造函数,则该转换构造函数的隐式转换功能失效。

一些类型转换的成员函数也可以用该关键字进行修饰以抑制隐式转换

如:

class Example
{
    public:
    explicit Example(int n){}//explicit抑制该构造函数的自动类型转换
    explicit operator int(){ return int{};}
};

void f(Example e){}

int main()
{
    Example e(1);
    //?  e = 2;         //错误:不能隐式转换(赋值运算)
    //?  f(3);          //错误:不能隐式转换(实参传给形参)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# (9)类型转换函数

其作用与转换构造函数相反,是将本类型转换为其他数据类型

格式:operator 函数名()

{ 函数体 …. return 想转换成的类型的数 }

如:

#include<iostream>
class RMB
{
    private:
    int yuan,jiao,fen;
    public:
    RMB(int y,int j,int f):yuan(y),jiao(j),fen(f){}
    
    RMB(double d = 0)//转换构造函数
    {
        int n = int(d * 100);//保留小数点后两位
        yuan = n / 100;
        jiao = (n - 100 * yuan) / 10;
        fen = n % 10;
    }
   
    operator toDouble()//类型转换函数
    {
        return yuan + jiao/10.0 + fen/100.0;
    }
    
    void Print()
    {
        cout<<yuan<<"元"<<jiao<<"角"<<fen<<"分"<<endl;
    }
};

int main()
{
    RMB r1(10,5,8);//构造对象
    r1.Print();//输出10元5角8分
    RMB r3(r1.toDouble());//转换构造函数,参数是类型转换函数
    r3.Print();//输出10元5角8分
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# (10)转移构造函数

一些对象在构造时可以获取其他对象(即将消亡)已有的内存资源,而不需要重新申请新的内存

当一个临时变量即将结束生命期时,将它所拥有的内存资源转移给其他对象,转移构造函数定义了在此过程中内存资源转移的方式

转移构造函数需要用到右值引用形式

原型:类名(类名&& 参数名)noexcept; //该函数通常声明为noexcept,即不抛出异常以确保异常安全

#include<iostream>
using namespace std;
class Square
{
    private:
    double length;
    public:
    Square(double l = 0)noexcept    //构造函数
    {
        length = l;
    }
    Square(const Square& s)         //复制构造函数(拷贝构造函数)
    {
        length = s.length;
    }
    Square(Square&& s)noexcept       //转移构造函数
    {
        length = std::move(s.length);
    }
    ~Square()noexcept
    {} 
};
Square MakeSquare(double d)          //工厂函数,以值形式返回结果
{
    Square s(d);            //构造了一个临时对象s
    return s;
}
int main()
{
    Square t{MakeSquare(10)};     //返回临时对象需要转移构造函数,MakeSquare(10)的返回值构造对象t
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

上例中:作为对比,类中还保留有复制构造函数。在类中没有转移构造函数而只有复制构造函数时,此种情况(函数MakeSquare中以值形式返回结果)会调用复制构造函数,根据返回值s复制构造t。但当类提供转移构造函数时,编译器会优先选用转移语义根据返回值s转移构造对象t。

转移构造会针对不同的数据成员采取不同的按位转移操作。

需要说明的是,对于右值引用来说,工具函数std::move()并不真正的将一个对象的资源转移给另一个对象,它只是实现了类似static_cast<T&&>(t) 的数据类型转换,将左值t转换为右值引用类型T&&。如果t所属的类支持转移语义,则进行转移构造;如果t所属的类不支持转移语义而只支持传统的复制语义,则进行复制构造。复制构造有浅复制和深复制之分,而转移构造只会进行“浅复制”操作

# 5.向函数传递对象

  • 使用对象作为函数参数

如:

void fun(Point point)//Point point = p;调用拷贝构造函数
{
    point.set(10,10);
    point.show();//打印Point的值
}

int main()
{
    Point p(1,1);
    fun(p);
    p.show();//打印p的值,未被改变
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 使用对象指针作为函数参数

如:

void fun(Point *point)//Point *point = &p;
{
    point->set(10,10);//等价于p.set(10,10);
    point->show();
}

int main()
{
    Point p(1,1);
    fun(&p);
    p.show();//打印p的值,被更改
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 使用对象的引用作为函数参数

如:

void fun(Point &point)//Point &point = p;
{
    point.set(10,10);//等价于p.set(10,10);
    point.show();
}

int main()
{
    Point p(1,1);
    fun(p);
    p.show();//打印p的值,被更改
}
1
2
3
4
5
6
7
8
9
10
11
12

# 6.类的静态成员

静态成员是指声明为static的成员,在类的范围内,所有对象共享该数据

静态成员可以声明为公有的,私有的,保护的

若声明为公有的,可直接访问,引用静态成员的格式为:

  1. <类名> :: <静态成员>
  2. 对象名 **. **公有静态成员
  3. 对象指针 -> 静态成员
  • 静态数据成员(常放在类和main函数外面)

    静态数据成员不属于任何对象,它在程序编译时创建并初始化,所以在该类的任何对象被创建前就存在

    初始化格式<数据类型> <类名> :: <静态数据成员名> = <初始值>;

  • 静态成员函数

    在一般函数前加static

    1. 一般情况下,静态成员函数主要用来访问全局变量或同一个类中的静态数据成员,可以用在建立任何对象前处理静态数据成员
    2. 静态成员函数不访问类中的非静态成员
    3. 静态成员函数中没有this指针
    4. 静态成员函数可以在类体内定义,也可以在类体外定义。在类体外时,不用static。

# 7.类的友元friend

使用友元的目的是提高程序运行效率

谨慎使用友元函数,因为它可以在类外直接访问类的私有或保护成员,破坏了类的信息隐藏特性

  • 友元函数

    1. 友元函数是类中说明的由关键字friend修饰非成员函数

    2. 友元函数不是当前类的成员函数,而是独立于当前类的外部函数,但它可以访问该类的所有对象的成员

    3. 在类中声明友元函数时,其原型为:friend <数据类型> <友元函数名> (<参数表>);

      此声明可以放在公有部分,也可以放在私有部分

    4. 友元函数可以定义在类内部,也可以定义在类外部

  • 友元成员

    1. 一个类的成员函数也可以作为另一个类的友元
    2. 这种成员函数不仅可以访问自己所在类对象中的所有成员,还可以访问friend声明语句所在类中的所有成员
    3. 这样能使两个类相互作用,协调工作,完成某一个任务
  • 友元类

    1. 友元还可以是类,即一个类可以作为另一个类的友元

    2. 当一个类作为另一个类的友元时,则该类中的所有成员函数都是另一个类的友元成员,都可以访问另一个类的所有成员

    3. 友元类的声明格式friend class 类名; //注意class别忘了

      此语句可以在公有、私有或保护部分

注意:

  1. 友元关系是单向的,不具有交换性,即类A中将类B声明为自己的友元类,但类B中没有将类A声明为自己的友元类,则类A的成员函数不可访问类B的私有成员,但类B可以访问类A的私有成员
  2. 当两个类都将对方声明为自己的友元时,才可以实现互访
  3. 友元关系也不具备传递性,即类A将类B声明为友元,类B将类C声明为友元,此时,类C不一定是类A的友元

# 8.类的复合

格式:class X

{

类名1 对象成员名1;

类名2 对象成员名2;

类名n 对象成员名n;

};

  • 对象成员的初始化问题

    一般来说,类X的构造函数的定义形式为:

    类名 (形参表):对象成员1(参数1),对象成员2(参数2),… ,…

{<构造函数体>}

  • 构造函数的调用顺序

    1.先对象成员所属类的构造函数,再本类中的构造函数

    2.如果对象成员不止一个,则按各对象在类中声明的顺序依次调用它们的构造函数,对这些对象初始化

    3.析构顺序与构造函数相反,且从外层向内层析构

上次更新时间:: 2/9/2025, 9:12:58 PM