【C++】类和对象(下)

简介: 构造函数初始化列表、类的static成员、友元函数和友元类、内部类、匿名对象、拷贝对象时的一些编译器优化。

1. 再谈构造函数

1.1 初始化列表

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

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

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而==不能称作初始化==。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

当我们使用类实例化出一个对象时,是对对象的整体定义,而对象中的每一个成员变量又是在哪里定义的呢?下面我们就引出了我们的初始化列表。

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

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) 类中包含以下成员,必须放在初始化列表位置进行初始化。

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)

首先我们来看一下引用,我们知道引用是一个变量的别名,引用在定义的时候就必须初始化。同时,const作为只读常量,也必须在在定义的时候就初始化。

image.png

image.png

所以对于引用类型的变量和const修饰的成员变量必须放在初始化列表进行初始化。同时,对于没有默认构造函数的自定义类型来说,也必须放在初始化列表进行初始化。

typedef int DataType;
class Stack
{
   
   
public:
    Stack(size_t capacity)
        :_size(0)
        ,_capacity(capacity)
    {
   
   
        cout << "Stack(size_t capacity = 10)" << endl;

        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
   
   
            perror("malloc申请空间失败");
            exit(-1);
        }

        _size = 0;
        _capacity = capacity;
    }
    //...
    ~Stack()
    {
   
   
        cout << "~Stack()" << endl;
        if (_array)
        {
   
   
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType *_array;
    size_t    _size;
    size_t    _capacity;
};

class MyQueue {
   
   
private:
    Stack _pushST;
    Stack _popST;
    int _size = 0;
};

image.png
image.png

当然了初始化列表可以和构造函数进行配合使用,函数体内可以完成一部分其他的在构造函数中需要实现的功能。

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

无论我们我们是否在初始化列表进行初始化写,类的成员变量都会走初始化列表,类的自定义类型会调用他的默认构造函数来完成初始化工作。

💕 (3) 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

我们可以举个栗子来看一下这一特性:

class A
{
   
   
public:
    A(int a)
        :_a1(a)
        , _a2(_a1)
    {
   
   }
    void Print() {
   
   
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2;
    int _a1;
};
int main() {
   
   
    A aa(1);
    aa.Print();
}

image.png

这里的结果为什么是1和随机值呢?其实这是因为因为_a2的声明在_a1之前,所以在初始化列表中首先被执行的是 ==_a2(_a1)== ,因为_a1此时是随机值,所以给_a2初始化成了一个随机值。


1.2 explicit关键字

💕 构造函数类型转换

无论对于单参数的构造函数,还是多参数的构造函数,我们不仅仅可以使用构造和拷贝构造的方式来实例化对象,还可以使用直接赋值一个或者多个整数的方式来实例化一个对象。

image.png

这个过程是这样实现的:编译器会先进行类型转换,将用2022来构造一个临时的对象,然后用这个临时的对象对a1进行拷贝构造,a2也是同样的道理。

💕 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用。

class Date
{
   
   
public:
    explicit Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {
   
   
        cout << "Date 构造" << endl;
    };
    Date(const Date& d)
    {
   
   
        _year = d._year;
        _month = d._month;
        _day = d._day;
        cout << "Date 拷贝构造" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
   
   
    Date d = {
   
    2022,2,10 };
    return 0;
}

image.png


2. static成员

2.1 概念

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

下面我们通过一道面试题来看一下和static相关的知识点:

💕 面试题:==实现一个类,计算程序中创建出了多少个类对象。==

int Count = 0;
class A{
   
   
public:
    A(int a = 0) {
   
    
        _a = a;
        ++Count;
    }
    A(const A& t){
   
   
        ++Count;
    }
private:
    int _a;
};

void TestA()
{
   
   
    A aa1;
    A aa2(aa1);
    A aa3 = 1;
    A aa4[10];
}
int main()
{
   
   
    TestA();
    cout << Count << endl;
    return 0;
}

这道题目我们常规的做法就是定义一个全局变量,然后将它放在构造和拷贝构造函数进行自增,但是这种方法并不是很好,因为全局变量在任何地方都可以被修改,而且还容易和库中的变量造成命名冲突。下面我们引入另一种方法:使用静态成员变量。


2.2 特性

静态成员变量有如下特性:

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
    image.png

  3. 类静态成员即可用 ==类名::静态成员== 或者 ==对象.静态成员== 来访问

  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

在了解了静态成员变量的相关知识后,我们就可以使用静态成员变量来计算类所创建的对象的个数了。

class A
{
   
   
public:
    A(int a = 0)
    {
   
   
        ++count;
    }
    A(const A& aa)
    {
   
   
        ++count;
    }
    // 静态成员函数 -- 没有this指针
    static int GetCount()
    {
   
   
        return count;
    }
private:
    static int count; // 声明
    int _a = 0;
};
int A::count = 0;

void func()
{
   
   
    A aa1;
    A aa2(aa1);
    A aa3 = 1;
    A aa4[10];
}
int main()
{
   
   
    func();
    cout << A::GetCount() << endl;
    return 0;
}

当然我们在程序中可以看到有个叫静态成员函数的东西,那么什么是静态成员函数呢?其实静态成员函数是指==用static关键字修饰的成员函数==,关于静态成员函数,我们需要注意两点:

  • 静态成员函数没有隐藏的this指针,所以不能访问非静态成员变量。
  • 静态成员也是类的成员,同样受类域和访问限定符的约束。
  • 静态成员函数不能访问非静态成员变量,但是非静态成员函数可以调用静态成员变量。

最后我们使用静态成员来做一个练习题:

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
【习题链接】

class Sum {
   
   
public:
    Sum()
    {
   
   
        _sum += _i;
        _i++;
    }
    static int GetSum()
    {
   
   
        return _sum;
    }
private:
    static int _i;
    static int _sum;
};
int Sum::_i = 1;
int Sum::_sum = 0;

class Solution {
   
   
public:
    int Sum_Solution(int n) {
   
   
        Sum* ptr = new Sum[n];
        return Sum::GetSum();
    }
};

3. 友元

3.1 友元函数

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

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

下面我们以Date类为例来看一下友元函数的用法:

class Date
{
   
   
    friend ostream& operator<<(ostream& _cout, const Date& d);
    friend istream& operator>>(istream& _cin, Date& d);
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {
   
   }
private:
    int _year;
    int _month;
    int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
   
   
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
   
   
    _cin >> d._year >> d._month >> d._day;
    return _cin;
}

image.png

这里我们来总结一下友元函数的几点特性:

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

3.1 友元类

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

这里我们来举一个例子:

class A
{
   
   
    friend class C;
private:
    static int k;
    int h = 1;
};

class C {
   
   
public:
    void foo(const C& a)
    {
   
   
        cout << "void foo(const C & a)" << endl;
    }
    void SetA()
    {
   
   
        _a.h = 1000;
        _a.k = 100;
    }
private:
    int b = 2;
    A _a;
};

在这里C是A的友元类,所以类C中可以访问类A中的成员变量,但是类A却不能访问类C中的成员变量,这是因为友元关系是单向的。

下面我们来看一下友元类的特点:

  • 友元关系是单向的,不具有交换性。
    比如上述A类和C类,在A类中声明C类为其友元类,那么可以在C类中直接
    访问A类的私有成员变量,但想在A类中访问C类中私有的成员变量则不行。
  • 友元关系不能传递
  • 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍

4. 内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

💕 注意: ==内部类就是外部类的友元类==,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

//内部类 和 友元类
class A
{
   
   
public:
    // 内部类 -- 跟A是独立,只是受A的类域限制
    // B天生就是A的友元
    class B {
   
   
    public:
        void foo(const A& a)
        {
   
   
            cout << k << endl;
            cout << a.h << endl;
        }
        int b = 2;
    };
private:
    static int k;
    int h = 1;
};

下面我们来看一下内部类的特性:

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。

这里我们还需要注意一点:==内部类的创建外部类类域和访问限定符的限制。==

image.png
image.png


5. 匿名对象

在C++中,我们可以直接使用类名来创建匿名对象,匿名对象和普通对象一样,创建和销毁的时候都会调用他的构造和析构函数,但是有一点我们需要注意,==匿名对象的生命周期只有它定义的那一行。==

class A
{
   
   
public:
    A(int a = 0)
        :_a(a)
    {
   
   
        cout << "A(int a)" << endl;
    }
    ~A()
    {
   
   
        cout << "~A()" << endl;
    }
private:
    int _a;
};
int main()
{
   
   
    //匿名对象的定义
    A();
    A(10);
    return 0;
}

6. 拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。

class A
{
   
   
public:
    A(int a = 0)
    :_a(a)
    {
   
   
        cout << "A(int a)" << endl;
    }
    A(const A& a)
        :_a(a._a)
    {
   
   
        cout << "A(const A& a)" << endl;
    }
    //重载=运算符
    A& operator=(const A& a)
    {
   
   
        cout << "A(const A& a)" << endl;
        if (this != &a)
        {
   
   
            _a = a._a;
        }
        return *this;
    }
    ~A()
    {
   
   
        cout << "~A()" << endl;
    }
private:
    int _a;
};

💕 传参优化:

void func1(A aa)
{
   
   }
void func2(const A& aa)
{
   
   }

int main()
{
   
   
    A a1 = 1;//构造 + 拷贝构造 -> 构造
    cout << "-----------------" << endl;
    func1(a1);  //无优化
    func1(2);   //构造 + 拷贝构造 -> 优化为直接构造
    func1(A(3));//构造 + 拷贝构造 -> 优化为直接构造
    cout << "-----------------" << endl;
    func2(a1);    //无优化
    func2(2);     //无优化   
    func2(A(3));  //无优化
    return 0;
}

💕 对象返回优化:

A func3()
{
   
   
    A aa;
    return aa; 
}
A func4()
{
   
   
    return A();
}
int main()
{
   
   
    func3();
    cout << "-----------------" << endl;
    A aa1 = func3();//拷贝构造 + 拷贝构造 -> 一个拷贝构造
    cout << "-----------------" << endl;
    A aa2;
    aa2 = func3();// 不能优化
    cout << "-----------------" << endl;
    func4();      //构造 + 拷贝构造 -> 一个拷贝构造
    A aa3 = func4();//构造 + 拷贝构造 + 拷贝构造 -> 一个构造
    return 0;
}

这里我们分别对传参对象返回进行一下总结:

image.png


7. 再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现
实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创
建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什
    么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清
    楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、
    Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣
    机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才
    能洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那
些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化
具体的对象。

image.png


相关文章
|
2天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
2天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
2天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
2天前
|
C语言 C++
【C++】日期类Date(详解)③
该文介绍了C++中直接相减法计算两个日期之间差值的方法,包括确定max和min、按年计算天数、日期矫正及计算差值。同时,文章讲解了const成员函数,用于不修改类成员的函数,并给出了`GetMonthDay`和`CheckDate`的const版本。此外,讨论了流插入和流提取的重载,需在类外部定义以符合内置类型输入输出习惯,并介绍了友元机制,允许非成员函数访问类的私有成员。全文旨在深化对运算符重载、const成员和流操作的理解。
|
2天前
|
定位技术 C语言 C++
C++】日期类Date(详解)①
这篇教程讲解了如何使用C++实现一个日期类`Date`,涵盖操作符重载、拷贝构造、赋值运算符及友元函数。类包含年、月、日私有成员,提供合法性检查、获取某月天数、日期加减运算、比较运算符等功能。示例代码包括`GetMonthDay`、`CheckDate`、构造函数、拷贝构造函数、赋值运算符和相关运算符重载的实现。
|
2天前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
2天前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
2天前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。
|
5天前
|
存储 编译器 C语言
【C++航海王:追寻罗杰的编程之路】类与对象你学会了吗?(上)
【C++航海王:追寻罗杰的编程之路】类与对象你学会了吗?(上)
10 2
|
4天前
|
C++
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
7 0
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)