【C++】类与对象

发布于:2025-05-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

1、类的定义

2、类的访问限定符及封装

3、类的实例化

4、类和对象的大小 

5、this 指针

 6、类的六个默认成员函数

构造函数 

析构函数

拷贝构造函数 

赋值重载函数 

取地址运算符的重载函数

7、运算符重载

8、const 成员函数

9、 static 成员

10、友元

11、内部类 

 12、匿名类


C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。

struct Stack
{
    // 成员函数
    void Init(int defaultCapacity = 4)
    {
        a = (int*)malloc(sizeof(int) * capacity); 
        if (nullptr == a)
        {
            perror("malloc申请空间失败");
            return;
        }
        capacity = defaultCapacity; 
        top = 0;
    }

    void Push(int x)
    {
    // 扩容 
    a[top++] = x;
    }

    void Destroy()
    {
        free(a); 
        a = nullptr; 
        top = capacity;
    }

    //...


    // 成员变量
    int* a; 
    int top;
    int capacity;
};

int main()
{
    Stack s; 
    s.Init(10); 
    s. Push(1); 
    s.Push(2); 
    s.Push(3); 
    cout << s. Top () << endl; 
    s.Destroy(); 
    return 0;
}

1、类的定义

class className
{
    // 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号

class为定义类的关键字,ClassName为类的名字,{  } 中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性成员变量;类中的函数称为类的方法或者成员函数

类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加 类名 ::

成员变量命名规则建议: 

// 我们看看这个函数,是不是很僵硬? 
class Date
{
    public:
    void Init(int year)
    {
        // 这里的year到底是成员变量,还是函数形参? 
        year = year;
    }
    
    private:
    int year;
};

// 所以一般都建议这样
class Date
{
    public:
    void Init(int year)
    {
        _year = year;
    }
    
    private:
    int _year;
};

// 或者这样
class Date
{
    public:
    void Init(int year)
    {
        mYear = year;
    }

    private:
    int mYear;
};

// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。

2、类的访问限定符及封装

访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。 

【访问限定符说明】
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

C++中 struct 和 class 的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class 定义的类默认访问权限是private。

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

3、类的实例化

用 类 类型创建对象的过程,称为类的实例化
1. 类是对对象进行描述的,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

4、类和对象的大小 

类和对象的存储方式:

在定义一个对象后,对象的存储的是它的数据成员,成员函数没有在对象内部。

同一个类和它实例化后的对象,sizeof(类)和 sizeof(对象)的结果是相同的。

类有内存对齐的现象:

空类和无数据成员的类的大小: 

// 类中仅有成员函数
class A2 
{ 
    public:
    void f2() {}
};

// 类中什么都没有 --- 空类
class A3 
{};

 sizeof(A2)和 sizeof(A3)的结果都是 1 ,为什么?

没有成员变量的类对象,需要 1 字节的原因是占位,表示对象存在(如果一个对象的大小是 0 字节,那么对象取地址怎么办呢?)

5、this 指针

同一个类定义的不同对象,各自调用它们的成员函数时,为什么每个对象的成员函数的接收到的形参都是各自对象的实参,明明都是调用的同一个函数。

某类有这样的一个成员函数:

void Print()
{
    cout << _ year << "-" <<_ month << "-" <<_ day << endl;
}

经过编译器处理后,它变成了这样:

void Print(Date* this)
{
    cout << this->_ year << "-" <this->_ month << "-" <<this->_ day << endl;
}

this 是指向对象的指针,哪个对象调用了 Print 函数,this 指针就指向谁。

1、this 指针不能在实参和形参中显式使用,但可以在函数内部显式使用。

2、this 指针是 类名* const 类型的,在函数中不允许修改它的指向。

3、this是形参,所以this指针是跟普通参数一样存储在函数调用的栈帧里面。

// 1. 下面程序编译运行结果是?
A、编译报错
B、运行崩溃
C、正常运行
class A
{
public:
    void Print()
    {
    cout << "Print()" << endl;
    }
private:
    int _a;
};

int main()
{
    A* p = nullptr; 
    p->Print();
    return 0;
}

答案是 C。

 p调用Print,不会发生解引用,因为Print的地址不在对象中。p会作为实参传递给this指针。在Print函数中,没有对 this 进行解引用操作。

如果将 Print 函数该为下面的样子,答案就是 B:

void Print()
{
    cout << _a << endl;
}

现在 Print 函数存在对 this 的解引用,而 this 接收的实参是 p,p 是 nullptr。

有人说 p->Print();  存在对 p 的解引用操作,但是从汇编代码来看,p->Print(); 只是将 p 作为实参传递给 this。

 6、类的六个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成6个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

用户显示实现默认成员函数时,不能在类外实现,否则会与编译器默认生成的成员函数冲突,可以在类中声明,在类外实现。

构造函数和析构函数是为了解决频繁的初始化和销毁工作,减少因为忘记初始化和销毁带来的问题。

构造函数 

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。其特征如下:

1. 函数名与类名相同。
2. 无返回值。(不需要写 void)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。全缺省的构造函数与无参构造函数构成重载,但调用时存在歧义。

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

6、编译器默认生成的构造函数对基本(int、double等等)类型不做处理,对自定义类型(class、struct)会去调用它的默认构造函数。(有些编译器对一个类默认生成的构造函数,如果该类有自定义数据成员和基本类型成员时,也会将基本类型初始化为 0,但不是所有的编译器都会这样做)

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认构造函数。默认构造函数就是不传参就可以调用的函数

示例:

class A
{
public:
    // 有参构造函数
    A(int a)
    {
        _a = a;
    }
    // 无参构造函数
    A()
    {
        _a = 1;
    }
    void Print()
    {
    cout << "Print()" << endl;
    }
private:
    int _a;
};

int main()
{
    A a(10);// _a 的初始值是 10
    A a; // _a 的初始值是 1;

    return 0;
}

注意:调用自己写的无参构造函数不能写成这样:

A a(); // error

原因:编译器会误以为 A a();是函数声明:函数名是 a,没有参数,返回值类型是 A 类。  

而应该这样: 

A a;

什么时候要写构造函数? 

1、一般情况下,一个类有基本类型成员,就需要自己写构造函数。

2、如果类中全部都是自定义类型成员,可以考虑使用编译器自己生成的默认构造函数。

3、类中的基本类型成员都有缺省值,这时可以不写构造函数。

类中的内置类型成员可以给缺省值

C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。如:

class A
{
public:
    void Print()
    {
    cout << "Print()" << endl;
    }
private:
    int _a = 1;
};

在创建 A 类对象时,_a 的默认值是 1。但是如果我们自己写了构造函数,并在定义对象时初始化,该缺省值就没有用了。 

 初始化列表:

除了在构造函数的函数体内对成员变量赋值的方式来初始化对象,还有一种初始化对象的方式,即初始化列表。

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

class Date
{
public:
Date(int year, int month, int day)
    :_ year(year) 
    ,_month(month) 
    ,_day(day)
{}
private: 
    int _year; 
    int _month; 
    int _day; 
};

注意:

1. 每个成员变量在初始化列表中只能出现一次(也可以不出现)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:(而不能在构造函数的函数体内初始化)
1)引用成员变量
2)const成员变量

引用成员变量和const成员变量的共同特征是必须在定义的时候初始化(const int a = 10,int& b = a)而一个普通变量(如 int 类型的变量),在定义时可以不初始化(int a;)。在定义对象时,引用成员变量和const成员变量随之定义,如果在构造函数的函数体内通过赋值的方式来初始化这些成员的话,就不满足在定义的时候初始化,初始化列表就是为了解决这样的问题。
3)自定义类型成员(且该类没有默认构造函数时)

3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序。
 

class A
{
public:
    A(int a)
    :_a1(a)
    ,_a2(_a1)
    {}
   
    void Print() 
    {
        cout <<_ al << " " <<_ a2 << endl;
    }
private:
    int _a2; 
    int _a1;
}

int main()
{
    A a(1);
    a.Print();

    return 0;
}

程序输出:1   随机值

原因:先定义 _a2,再定义 _a1,所以初始化列表的执行顺序是:先执行 _a2(_a1) 这时 _a1 还是随机值,赋值给了_a2,再执行_a1(a)。

初始化列表不能完全代替构造函数,它是构造函数的一部分:

class Stack
{
public:
    Stack(int capacity = 10)
        //初始化列表
        :_ a((int*)malloc(capacity * sizeof(int))) 
        ,_top(0) 
        ,_capacity(capacity)
    {
        //初始化列表不能完成的任务
        if (nullptr == _ a)
        {
            perror("malloc申请空间失败”);
            exit(-1);
        }
        // 要求数组初始化一下
        memset(_a, 0, sizeof(int) * capacity);
    }
private:
    int* _ a;
    int _top; 
    int _capacity;
}

动态开辟二维数组:

class AA
{
public:
    AA(int row = 10, int col = 5)
      :_ row(row)
      ,_col(col)
    {
        _aa = (int**)malloc(sizeof(int*) * row); // _aa 数组是指针数组,每个数组元素都指向一个 
                                                 // 一维数组
        
        for (int i = 0; i < row; i++)
        {
            aa[i] = (int*)malloc(sizeof(int) * col);
        }
private:
    int ** _ aa; 
    int _row; 
    int _col;
};

析构函数

概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

特性

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型(不加 void)。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

5、编译器默认生成的析构函数对基本(int、double等等)类型不做处理,对自定义类型(class、struct)会去调用它的析构函数。

6、对象析构的顺序满足“后定义的先析构”。

什么时候写析构函数 

1、一般情况下,有动态申请的堆上的内存空间,就需要写析构函数释放内存空间

2、没有动态申请的的堆上的内存空间,不需要写析构函数

3、需要释放空间的成员都是自定义类型,不需要写析构函数

构造函数和析构函数的优点:构造函数和析构函数是为了解决频繁的初始化和销毁工作,减少因为忘记初始化和销毁带来的问题。

拷贝构造函数 

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

类名( const 类名& 对象名){ }

特征

1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。(调用拷贝构造函数要传递参数,传递参数要拷贝实参给形参,又要调用拷贝构造函数,又要传递参数,传递参数要拷贝实参给形参,又要调用拷贝构造函数...)

3. 若未显式定义,编译器会生成默认的拷贝构造函数。对基本类型完成值拷贝(浅拷贝),对自定义类型,调用它的拷贝构造函数。

示例:

在定义 Date 类时,有如下拷贝构造函数:

void Date(const Date& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

使用拷贝构造函数:

Date d1(d2);

定义日期类对象 d1,并用 d2(已存在)初始化。 

C++ 规定,基本类型直接拷贝,自定义类型必须调用拷贝构造函数来拷贝。如果函数的形参是自定义类型的引用,不再调用拷贝构造函数,且实参与形参之间没有发生拷贝。

若类中有指针变量成员,该类的对象使用默认拷贝函数拷贝时,拷贝后两个对象的指针成员都指向同一块内存空间(这不是我们期望的拷贝结果,我们期望的是两个指针成员指向两块内存空间,这两块内存空间的存储内容一样),这块内存空间就会被析构两次,程序会崩溃。

与赋值重载函数的区别 

// 已经存在的两个对象之间复制拷贝 -- 运算符重载函数
Date d1(2024,5,1);
Date d2(2024,6,1);
d1 = d2;

// 用一个已经存在的对象初始化另一个对象 -- 构造函数
Date d1(2024,5,1);

Date d3(d1);
//或
Date d3 = d1;

注意:不是只要有 = 就要调用运算符重载函数,如:Date d4 = d1;d1 是已经定义并初始化的对象,这时是调用的拷贝构造函数。

编译器对拷贝构造的优化

class A
{
public:
    A(int a)
    {
        _a = a;
    }
private:
    int _a;
}

func(A aa)
{}

int main()
{
    A aa = 1;//构造+拷贝构造 --> 优化为构造

    A aa1;
    func(aa1);//不在同一行,不优化

    func(A(1)); //构造+拷贝构造 --> 优化为构造
    func(1); //构造+拷贝构造 --> 优化为构造
}

如果不进行优化,则使用 1 去构造一个 A 类临时对象, 再用这个临时对象去拷贝构造 aa,编译器经过优化以后,直接用 1 构造 aa。

注意,要在同一行的连续拷贝构造或构造才会优化

赋值重载函数 

对 = 进行运算符重载,实现两个同类的对象之间的赋值。

class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    void operator=(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
}

 如有 d1 = d2;则调用 d1 的 operator= 函数。以上代码存在一个问题,就是对象之间不能连续赋值,比如:d1 = d2 = d3;这是因为 operator= 函数的返回值是 void(d2 = d3,这个表达式的值是 void)。对 operator= 函数做如下改进:

Date& operator=(const Date& d)
{
    if( this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    return *this;
}

1、*this 出了 operator= 函数后还存在,所以可以使用引用返回,减少对象的拷贝(函数返回值要调用拷贝构造函数拷贝到寄存器,再由寄存器将值赋给表达式) 

2、为了可以检测自己给自己赋值,可以加一个 if 判断。

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。默认赋值运算符重载对内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。 

取地址运算符的重载函数

此函数如果我们不写,编译器会默认生成以下函数:

Date* operator&()
{
    return this;
}

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

这两个函数构成了函数重载:第一个 operaor& 函数的形参是 Date* this,第二个函数的形参是 const Date* this。

有时候,我们不想让别人获取到非常对象的地址,而常对象的地址可以获取,就可以这样做:

Date* operator&()
{
    return nullptr;
}

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

7、运算符重载

有以下一个类:

class Date
{
public:
    int _year;
    int _month;
    int _day;
}

如果要比较两个日期类对象的先后,一般是写一个函数,通过函数调用返回布尔值来判断:

bool less(const Date& x1, const Date& x2)
{
    if (x1 ._year > x2 ._year)
    {
        return true;
    }
    else if (x1 ._ year == x2 ._ year && x1 ._ month > x2 ._ month)
    {
        return true;
    }
    else if (x1 ._ year == x2 ._ year && x1 ._ month == x2 ._ month && x1 ._ day > x2 ._ day)
    {
        return true;
    }
    
    return false;
}

能否使用 > 来直接判断呢?比如:d1 > d2,只要将函数名该为:operator< 即可。

bool operator<(const Date& x1, const Date& x2)
{
    if (x1 ._year > x2 ._year)
    {
        return true;
    }
    else if (x1 ._ year == x2 ._ year && x1 ._ month > x2 ._ month)
    {
        return true;
    }
    else if (x1 ._ year == x2 ._ year && x1 ._ month == x2 ._ month && x1 ._ day > x2 ._ day)
    {
        return true;
    }
    
    return false;
}

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:

返回值类型 operator操作符(参数列表)
注意:

● 不能通过连接其他符号来创建新的操作符:比如 operator@
● 重载操作符必须有一个类类型参数,重载操作符有几个操作数,就有几个参数。
● 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
● 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的 this

(以上代码仍有问题:_year、_month、_day 一般是私有的数据成员,在类外不能直接访问私有成员,一个解决方法是将 operator< 函数作为 Date 类的成员函数:)

调用上面的函数时,可以这样调用:d1 > d2,也可以这样:d1.opreator<(d2)。

● .*(点和星号)::(域作用限定符)sizeof (求大小) ?:(三目)注意这 5个运算符不能重载。

 实现运算符重载的技巧:

1、先实现 operator<(上面已实现) 和 operator== :

bool operator==(const Date& d)
{
    return _year == d._year
           && _month == d._month;
           && _day == d._day;
}

2、有了 operator< 和 operator==,那么 operator<= 和 operator >=只需:

bool Date::operator<= (const Date& x)
{
    return *this < x | | *this == X;
}
bool Date::operator>= (const Date& x)
{
    return !(*this < x);
}

 3、有了 operator<= ,operator> 只需:

bool Date::operator>(const Date& x)
{
    return !(*this <= x);
}

实现 Date 类 加上天数的 + 重载:

Date& Date::operator+(int day)
{
    day += day; 
    while(_day > GetMonthDay(_year, _month))
    {
        day -= GetMonthDay(_year, _month); 
        ++_month;
        
        if(_month == 13)
        {
            ++_year; 
            month = 1;
        }
    }

    return *this;
}

这样,Date 类的对象就能计算加上某个天数后的年月日,比如:

int main()
{
    Date d1(2025,5,1);
    d1 + 100;
    d1.print();

    return 0;
}

代码有些失误,实际上上面代码实现的是 operator+=,而不是 operator+,比如 int i = 0;i + 10;i 的值并没有改变。问题出在上面代码对 Date 类型直接 + 了 。

Date& Date::operator+=(int day)
{
    day += day; 
    while(_day > GetMonthDay(_year, _month))
    {
        day -= GetMonthDay(_year, _month); 
        ++_month;
        
        if(_month == 13)
        {
            ++_year; 
            month = 1;
        }
    }

    return *this;
}

+= 也有返回值的意义是实现连续的 +=,比如 i += j += 10。重复使用 operator+= 实现 + :

Date Date::operator+(int day)
{
    Date tmp(*this);

    tmp._day += day; 
  
    return tmp;
}

1、使用拷贝构造函数来初始化 tmp,不能对 *this 直接 +

2、tmp 出了函数就被销毁,所以不能用引用返回。

3、可以用 operator+ 反过来实现 operator+=,但不如上面的实现方法,因为上面的实现方法只创建了两个对象,而这种方法创建了四个对象。推荐先实现 += ,再实现 +,减法类似

4、以上代码不能应对 d1 += -100 这种情况,在实现 -= 和 - 后,在 operator+ 中加 if 判断,operator- 也是。

前置++和后置++:

我们已经知道前置++和后置++的规则:

Date d1(2025,5,1)

d1++;
++d1;
//d1 都要 +1,++d1 返回 +1 后的 d1,d1++ 返回 +1 前的 d1

那具体怎么实现呢?

operator++ 默认是前置 ++:

// 前置++ 
Date& Date::operator++()
{
    *this += 1; 
    return *this;
}

+= 已重载(见上文)

那后置 ++ 如何实现呢?

// 后置++ 
//增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成函数重载
Date Date::operator++(int)
{
    Date tmp = *this;
    *this += 1;
    return tmp;
}

对类对象的输入与输出的操作符重载

流插入操作符 << 是 C++ 新增的操作符,在库里面已经实现了它对内置类型的重载:

对于自定义类型,比如 Date 类型,直接使用 cout<< d1 << endl;是不行的(d1 是 Date 类的对象),需要对 << 进行运算符重载。 

void Date::operator<<(ostream& out)
{
    out<<_ year<<"年"<<_ month<<"月"<<_ day<<"日"<<endl;
}

但是如果我们用 cout << d1 ;来使用重载后的操作符,会发现程序编译不通过,其实要这样写才能使用重载后的操作符:d1 << cout;这违背了我们的使用习惯。所以 << 的重载函数不能写成 Date 类的成员函数,而应该写成全局函数。

void operator<<(ostream& out,const Date d)
{
    out<<d._ year<<"年"<<d._ month<<"月"<<d._ day<<"日"<<endl;
}

新的问题由出现了:operator<< 是全局函数,无法直接访问 Date 类对象的数据成员。有两个解决办法:

1、在 Date 类里再增加这些成员函数:

int Getyear()
{
    return _year;
}

operator<< 函数写成:

void operator<<(ostream& out,const Date d)
{
    out<<d.Getyear()<<"年"<<d.Getmonth()<<"月"<<d.Getday()<<"日"<<endl;
}

2、将  operator<< 作为 Date 类的友元函数。

以上代码无法实现连续打印:cout << d1 << d2 << d3;解决办法是将 operator<< 的返回值设为 ostream 类:

ostream& operator<<(ostream& out,const Date d)
{
    out<<d._ year<<"年"<<d._ month<<"月"<<d._ day<<"日"<<endl;

    return out;
}

类对象的输入:

istream& operator>>(istream& in, Date& d)
{
    in >> d. year >> d. month >> d ._ day; 

    return in;
}

8、const 成员函数

常对象只能调用常成员函数

d2 不能调用 Print 函数是因为 d2 是常对象,常对象在调用非常函数时,传递给 this 指针是 const Date* 类型的地址,权限被放大。  

怎么让 d2 也可以调用 Print 函数呢?当然是将 this 指针转换成 const Date* 类型的指针变量,由于不能在实参和形参中显式使用 this 指针,所以,用 const 修饰 this 指针的方法是:

void Print() const

 const 不是修饰 Print 函数的,是修饰 this 指针的。

哪些成员函数可以加 const 

如果一个成员函数没有修改对象数据成员的打算(单纯打印数据、对 + 、-、*、/ 、>、<等运算符重载的函数等等),就可以加 const。如果成员函数可能修改对象的数据成员,就不能加 const,否则函数的功能不能实现。如果要加 const,则在声明和定义都要加。

请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?

在类中,加上 const 和不加 const 的同名函数构成重载。 

9、 static 成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

静态成员变量:

1、不属于类的每个对象,它属于类,即被类的每个对象所共享。

2、它必须在类外定义,定义时不加 static,类中只是声明(没有为什么,就是规定)。

3、它通常是私有的,要配合使用静态成员函数访问。

4、它存储在静态区,sizeof()不包含静态成员变量的大小。

静态成员函数:

1、通常与静态成员变量同时出现,用于获取静态成员变量,

2、它没有this指针,不能访问对象的成员,

3、它通过类名和域访问限定符就可以访问

4、类中其他非静态的成员函数可以调用静态成员函数,但静态成员函数不能调用类的非静态函数,因为静态成员函数没有this指针

例子一:实现一个类,计算程序中创建出了多少个类对象。

class A
{
public: 
    A() { ++_scount; } 
    A(const A& t) { ++_scount; } 
    ~A() { --_scount; } 
    static int GetACount () { return _scount; }
private: 
    static int _scount; 
};

例子二:实现一个类,这个类只能在栈或堆上创建对象。

class A
{
public:
    static A GetStackObj ()
    {
        A aa; 
        return aa;
    }
    
    static A* GetHeapObj ()
    {
        return new A;
    }
private:
    A()
    {}
    
    int _a1 = 1; 
    int _a2 = 2;
};

上面的 A 类中,构造函数被 private 访问限定符限定,如果我们直接创建一个 A 类的对象,是不行的,必须调用 GetStackObj () 函数或 GetHeapObj () 函数来创建 A 类的对象,但是如果要调用GetStackObj () 函数或 GetHeapObj () 函数,又必须有一个 A 类的对象,使用静态函数就可以解决这个问题,所以将GetStackObj () 函数和GetHeapObj () 函数声明为静态函数。

10、友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

问题:现在尝试去重载operator << ,然后发现没办法将operator << 重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator << 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

class Date
{
    //友元声明
    friend ostream& operator<<(ostream& out,const Date d);
public:
Date(int year, int month, int day)
    :_ year(year) 
    ,_month(month) 
    ,_day(day)
{}
private: 
    int _year; 
    int _month; 
    int _day; 
};

ostream& operator<<(ostream& out,const Date d)
{
    out<<d._ year<<"年"<<d._ month<<"月"<<d._ day<<"日"<<endl;

    return out;
}

注意:

● 友元函数可访问类的私有和保护成员,但不是类的成员函数
● 友元函数不能用const修饰
●友元函数可以在类定义的任何地方声明,不受类访问限定符限制
● 一个函数可以是多个类的友元函数
●友元函数的调用与普通函数的调用原理相同

 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

· 友元关系是单向的,不具有交换性。
在A类中声明B类为其友元类,那么可以在B类的成员函数中直接访问A类的私有成员变量,但想在A类中访问B类中私有的成员变量则不行。
· 友元关系不能传递
如果C是B的友元,B是A的友元,则不能说明C时A的友元。

11、内部类 

概念:如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性:

1. 内部类受外部类的public、protected、private访问限定符限定。
2. 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,不包含内部类。

4、在外部类外定义内部类的对象要使用:外部类名::内部类名 对象名

示例:

class A
{
private:
    static int k; 
    int h;
public:
    class B // B天生就是A的友元
    {
    public:
        void foo(const A& a)
        {
            cout << k << endl; //OK 
            cout << a.h << endl; //OK
        }
};

int A :: k = 1;

 12、匿名类


网站公告

今日签到

点亮在社区的每一天
去签到