C++笔记归纳2:类和对象

类和对象

目录

类和对象

一、类的定义

1.1.类的格式

1.2.访问限定符

1.3.类域

二、实例化

2.1.实例化概念

2.2.对象大小

2.3.内存对齐规则

三、this指针

3.1.this指针的概念

[四、C++ VS C实现栈](#四、C++ VS C实现栈)

4.1.C++的优势

五、类的默认成员函数

5.1.默认成员函数的概念

5.2.六个默认成员函数

5.3.构造函数

5.4.析构函数

5.5.拷贝构造函数

5.6.运算符重载

5.7.赋值运算符重载

5.8.const成员函数

5.9.取地址运算符重载

六、日期类的实现

6.1.头文件编写

6.2.源文件编写

七、再探构造函数

7.1.初始化列表

八、类型转换

8.1.隐式类型转换

九、static成员

9.1.静态成员变量的概念

9.2.静态成员变量的特点

9.3.静态成员函数的概念

9.4.静态成员函数的特点

十、友元

10.1.友元的概念

10.2.友元的实现

10.3.友元函数的特点

十一、内部类

11.1.内部类的概念

11.2.内部类的特点

11.3.内部类的本质

十二、匿名对象

12.1.匿名对象的概念

12.2.匿名对象的特点

十三、对象拷贝时的编译器优化


一、类的定义

1.1.类的格式

**class:**定义类的关键字

**Stack:**类的名字

**{}:**类的主体

类的成员:

类中的变量为成员变量(类的属性)

类中的函数为成员函数(类的方法)

cpp 复制代码
#include <iostream>
using namespace std;

class Stack
{
	//成员函数
	void Push(int x)
	{
		//...
	}
	void pop()
	{
		//...
	}
	int top()
	{
		//...
	}
	//成员变量
	int* a;
	int top;
	int capacity;
};

为了区分成员变量,习惯给成员变量

加一个特殊的标识,比如_或m开头

cpp 复制代码
class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    private:
        int _year; // year_  m_year
        int _month;
        int _day;
};

int main()
{
    Date d;
    d.Init(2024, 3, 31);
    return 0;
}

C++中struct也可以定义类

C++兼任了C语言中struct的用法,同时struct升级成了类

区别:

C++的struct中可以定义函数,而C语言的struct只能声明函数指针

C++的struct名称就可以代表类型,不需要再typedef,去掉struct

cpp 复制代码
#include<iostream>
using namespace std;
// C++升级struct升级成了类
// 1、类⾥⾯可以定义函数
// 2、struct名称就可以代表类型
// C++兼容C中struct的⽤法
typedef struct ListNodeC
{
    struct ListNodeC* next;
    int val;
}LTNode;

//不再需要typedef,ListNodeCPP就可以代表类型
struct ListNodeCPP
{
    void Init(int x)
    {
        next = nullptr;
        val = x;
    }
    ListNodeCPP* next;
    int val;
};

定义在类里面的成员函数默认为inline

声明在类里面,但定义不在类中的成员函数不为内联函数

cpp 复制代码
//内联函数

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

//非内联函数

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

void Date::Init(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

1.2.访问限定符

C++用类将对象的属性与方法结合在一起,让对象更加完善

通过访问权限,选择性地将其接口提供给外部的用户去使用

**public:**修饰的成员在类外可以直接被访问

**protect:**修饰的成员在类外不能直接访问

**private:**修饰的成员在类外不能直接访问

作用域:

从该访问限定符出现开始的位置到下一个访问限定符出现时为止

如果后面没有访问限定符,则作用域到}结束

class定义成员没有被访问限定符修饰时默认为private

struct定义成员没有被访问限定符修饰时默认为public

cpp 复制代码
#include <iostream>
#include <assert.h>
using namespace std;

class Stack
{
    //私有
    void Push(int x)
       {
           // ...扩容
           array[top++] = x;
       }
public://公有
    void Init(int n = 4)
    {
        array = (int*)malloc(sizeof(int) * n);
        if (nullptr == array)
        {
            perror("malloc申请空间失败");
            return;
        }
        capacity = n;
        top = 0;
    }
    int Top()
    {
        assert(top > 0);
        return array[top - 1];
    }
    void Destroy()
    {
        free(array);
        array = nullptr;
        top = capacity = 0;
    }
private://私有
    int* array;
    size_t capacity;
    size_t top;
};

**注:**一般成员变量都会限制为私有,需要给别人使用的成员函数为公有

1.3.类域

**类域:**类定义的一个新的作用域

在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域

类域影响的是编译查找规则

示例:

Init函数如果不指定类域Stack,编译器就把Init函数当成全局函数

编译时,找不到array等成员的声明或定义在哪里,就会发生报错

Init函数如果指定类域Stack,就知道Init是成员函数,去类域查找

cpp 复制代码
#include<iostream>
using namespace std;

class Stack
{
public:
    //成员函数
    void Init(int n = 4);
private:
    //成员变量
    int* array;
    size_t capacity;
    size_t top;
};

// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
    array = (int*)malloc(sizeof(int) * n);
    if (nullptr == array)
    {
        perror("malloc申请空间失败");
        return;
    }
    capacity = n;
    top = 0;
}
int main()
{
    Stack st;
    st.Init();
    return 0;
}

二、实例化

2.1.实例化概念

**类实例化对象:**用类类型在内存中创建对象的过程

类是对象的一种抽象描述。是一个模型一样的东西

限定了类有哪些成员变量,这些成员变量只是声明

没有分配内存空间,用类示例化出对象才分配空间

类: 图纸 **对象:**用图纸建造的房子

一个类可以实例化多个对象

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	// 这⾥只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	// Date类实例化出对象d1和d2
	Date d1;
	Date d2;
    //d1.Init(&d1,2026, 2, 12);
	d1.Init(2026, 2, 12);
    //d1.Print(&d1);
	d1.Print();
    //d2.Init(&d2,2026, 2, 11);
	d2.Init(2026, 2, 11);
    //d2.Print(&d2);
	d2.Print();
	return 0;
}

2.2.对象大小

对象中只存储成员变量

函数被编译后是一段指令,对象无法存储

这些指令存储在单独的一个区域(代码段)

如果对象必须要存储函数,只能存储成员函数的指针,但函数指针是不需要存储的

示例:

Data实例化d1和d2两个对象,d1和d2都有各自独立的成员变量

_year,_month,_day存储各自的数据

而d1和d2的成员函数Init和Print指针相同

如果Data实例化100个对象,那么成员函数指针就要重复存储100次,造成浪费

注:

函数指针是一个地址,调用函数被编译成汇编指令(call地址)

编译器在编译链接时,就找到函数的地址

而不是在程序运行时,去寻找函数的地址

2.3.内存对齐规则

第一个成员在与结构体偏移量为0的地址处

其他成员变量要对齐到对齐数的整数倍的地址处

**注:**对齐数 = min(编译器默认对齐数,成员变量大小)

VS中默认的对齐数为8

**结构体总大小:**最大对齐数的整数倍

结构体嵌套:

嵌套的结构体对齐到直接的最大对齐数的整数倍

结构体的整体大小就是所有最大对齐数的整数倍

示例:

1.计算A实例化的对象

2.计算B和C实例化的对象

没有成员变量的类,它的对象大小为1

三、this指针

3.1.this指针的概念

编译器在编译时,类的非静态成员函数默认

在形参的第一个位置增加一个类类型的指针

该指针指向的是用类创建的对象地址

示例:

在Date类中有Init与Print两个成员函数,函数体中没有对不同的对象进行区分

那么函数该如何选择要访问的对象?C++中的隐式this指针就解决了这个问题

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	// void Init(Date* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		// 编译报错:error C2106 : " = " :左操作数必须为左值
		// this = nullptr;
		// this->_year = year;
		_year = year;
		this->_month = month;
		this->_day = day;
	}
	//void Print(Date* const this)
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//这⾥只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	// Date类实例化出对象d1和d2
	Date d1;
	Date d2;

	//d1.Init(&d1, 2026, 2, 12);
	d1.Init(2026, 2, 12);
	//d1.Print(&d1);
	d1.Print();

	//d2.Init(&d2, 2026, 2, 11);
	d2.Init(2026, 2, 11);
	//d2.Print(&d2);
	d2.Print();

	return 0;
}

**注:**类的成员函数访问成员变量,本质就是用this指针进行访问

不能在形参和实参的位置显示的写this指针,只可以在成员函数体内使用

this指针存在内存的栈区(作为形参),有的编译器也会优化到寄存器中

**问题1:**下面程序为什么能正常运行

p->Print():

编译时将p的值(nullptr)放入ecx寄存器(this指针)

然后直接通过call指令,调用A::Print函数的固定地址

p->_a:

当使用p访问A类型中的变量_a时,会发生报错

因为这里对空指针nullptr进行了解引用

**问题2:**下面程序为什么会发生报错

此时this指针为p(nullptr),在类的成员函数体内使用成员变量

本质上是对this解引用,相当于对空指针进行解引用,发生报错

**修改方案:**确保this指针指向一个有效对象

此时this指针为&obj,在类的函数体内解引用时,不会发生错误

四、C++ VS C实现栈

4.1.C++的优势

C++中数据和函数都放到了类里面

通过访问限定符进行了限制,不能再随意通过对象直接修改数据(封装)

C++中的语法相对方便:

  • Init的缺省参数,避免空间浪费
  • this指针隐式传址,不需传送对象地址
  • 不需要用typedef重命名

C++实现代码:

cpp 复制代码
#include <iostream>
#include <assert.h>
using namespace std;

typedef int STDataType;

class Stack
{
public:
    //成员函数
    void Init(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc 申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    } 

    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
                sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            _a = tmp;
            _capacity = newcapacity;
        }
        _a[_top++] = x;
    }

    void Pop()
    {
        assert(_top > 0);
        --_top;
    }

    bool Empty()
    {
        return _top == 0;
    }

    int Top()
    {
        assert(_top > 0);
        return _a[_top - 1];
    }

    void Destroy()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
private:// 成员变量
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);
    while (!s.Empty())
    {
        printf("%d\n", s.Top());
        s.Pop();
    }
    s.Destroy();
    return 0;
}

C语言实现代码:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;

void STInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

void STDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

void STPush(ST* ps, STDataType x)
{
	assert(ps);
	// 满了,扩容
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity *sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}

bool STEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}

void STPop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	ps->top--;
}

STDataType STTop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	return ps->a[ps->top - 1];
}

int STSize(ST* ps)
{
	assert(ps);
	return ps->top;
}

int main()
{
	ST s;
	STInit(&s);
	STPush(&s, 1);
	STPush(&s, 2);
	STPush(&s, 3);
	STPush(&s, 4);
	while (!STEmpty(&s))
	{
		printf("%d\n", STTop(&s));
		STPop(&s);
	}
	STDestroy(&s);
	return 0;
}

五、类的默认成员函数

5.1.默认成员函数的概念

用户没有显式实现,编译器会自动生成的成员函数

5.2.六个默认成员函数

初始化和清理

  • 构造函数:主要完成初始化工作
  • 析构函数:主要完成清理工作

拷贝复制

  • 拷贝构造:使用同类对象和初始化创建对象
  • 赋值重载:主要把一个对象赋值给另一个对象

取地址重载

  • 主要是普通对象和const对象取地址

编译器默认生成的函数行为是什么,是否满足我们需求

编译器默认生成的函数不满足我们需求,我们如何自己实现

5.3.构造函数

构造函数的概念:

特殊的成员函数,主要任务并不是开空间创建对象,而是在对象实例化时初始化对象

构造函数的本质:

利用构造函数自动调用的特点,代替Stack、Date类中的Init函数功能

构造函数的特点:

  • 函数名与类名相同
  • 无返回值(不需要写void)
  • 对象实例化时系统会自动调用对应的构造函数
  • 构造函数可以重载

1.无参构造函数

cpp 复制代码
#include < iostream>
using namespace std;
class Date
{
public:
	//1.⽆参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;//调⽤默认构造函数
	d1.Print();
	return 0;
}

注:

如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号

否则编译器⽆法区分是函数声明还是实例化对象

2.带参构造函数

cpp 复制代码
#include < iostream>
using namespace std;
class Date
{
public:
	//2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d2(2026, 2, 13);//调⽤带参的构造函数
	d2.Print();
	return 0;
}

3.全缺省构造函数

cpp 复制代码
#include < iostream>
using namespace std;
class Date
{
public:
	// 3.全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d0;
	d0.Print();
	Date d1(2026);
	d1.Print();
	Date d2(2026,2);
	d2.Print();
	Date d3(2026,2,13);
	d3.Print();
	return 0;
}

默认构造函数的概念:

如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的构造函数

这种编译器生成的构造函数,与无参构造函数,全缺省构造函数,都称为默认构造函数

注意:

这三个函数有且只有一个存在,不能同时存在

无参构造函数与全缺省构造函数虽然构成函数重载,但调用时会存在歧义

总结:

不传实参就可以调用的构造函数就叫默认构造函数

编译器生成的构造函数初始化缺陷:

对内置类型成员变量,是否初始化是随机的,取决于编译器

对自定义类型成员变量,需要调用这个成员变量的默认构造函数初始化

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:
	//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
	Stack pushst;
	Stack popst;
};

int main()
{
	MyQueue mq;
	return 0;
}

如果自定义类型成员变量的构造函数不为默认构造函数,那么编译器就会报错

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	Stack(int n)//此时不为默认构造函数
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

class MyQueue
{
public:
private:
	Stack pushst;
	Stack popst;
};

int main()
{
	MyQueue mq;
	return 0;
}

显然,由编译器生成的默认构造函数基本不能满足我们的需求

在大多数情况下,默认构造函数还是需要自己手动去编写实现

5.4.析构函数

析构函数的概念:

与构造函数功能相反,不是完成对对象本身的销毁(局部对象存在栈帧,函数结束时栈帧销毁)

而是完成对象中资源的清理释放工作,C++规定对象在销毁时会自动调用析构函数

析构函数的本质:

代替Stack类中的Destroy函数功能,而像Date类没有Destroy

其实没有资源需要释放,严格来说Date类是不需要析构函数的

析构函数的特点:

  • 函数名是在类名前加上字符~
  • 无参数无返回值(不需要写void)
  • 一个类只能有一个析构函数
  • 未显式定义时,系统会自动生成默认析构函数
  • 对象生命周期结束时,系统会自动调用析构函数
  • 一个局部域的多个对象,后定义的先析构

编译器生成的析构函数对内置类型成员不做处理

编译器生成的析构函数对自定义类型成员会调用它的析构函数

析构函数的使用规则:

如果类中没有申请内存资源时,析构函数可以不写,直接使用系统默认的析构函数,如:Date

如果默认生成的析构函数就可以用,也不需要显式写析构函数,如:MyQueue

如果有资源申请,那么一定要自己写析构函数,否则会资源泄露,如:Stack

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	//...

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列

class MyQueue
{
public:
	//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
	//即使显式写析构,也会自动调用Stack析构
	/*~MyQueue()
	* {
	* }
	*/
private:
	Stack pushst;
	Stack popst;
};

int main()
{
	Stack st1;

	MyQueue mq;

	return 0;
}

**示例:**对比C++与C实现的Stack解决括号匹配问题

C++实现代码:

cpp 复制代码
#include<iostream>
using namespace std;

// ⽤最新加了构造和析构的C++版本Stack实现
bool isValid(const char* s) 
{
    Stack st;
    while (*s)
    {
        if (*s == '[' || *s == '(' || *s == '{')
        {
            st.Push(*s);
        }
        else
        {
             //右括号⽐左括号多,数量匹配问题
             if (st.Empty())
             {
                return false;
             }
             //栈⾥⾯取左括号
             char top = st.Top();
             st.Pop();
             //顺序不匹配
             if ((*s == ']' && top != '[') || (*s == '}' && top != '{') || (*s == ')' && top != '('))
             {
                return false;
             }
        }
        ++s;
    }
    //栈为空,返回真,说明数量都匹配左括号多,右括号少匹配问题
    return st.Empty();
}

C语言实现代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// ⽤之前C版本Stack实现
bool isValid(const char* s) 
{
    ST st;
    STInit(&st);
    while (*s)
    {
         //左括号⼊栈
         if (*s == '(' || *s == '[' || *s == '{')
         {
             STPush(&st, *s);
         }
         else//右括号取栈顶左括号尝试匹配
         {
            if (STEmpty(&st))
            {
                STDestroy(&st);
                return false;
            }
            char top = STTop(&st);
            STPop(&st);
            // 不匹配
            if ((top == '(' && *s != ')')|| (top == '{' && *s != '}')|| (top == '[' && *s != ']'))
            {
              STDestroy(&st);
              return false;
            }
         }
         ++s;
    }
    //栈不为空,说明左括号⽐右括号多,数量不匹配
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}

5.5.拷贝构造函数

拷贝构造函数的本质:

一种特殊的有参构造函数

拷贝构造函数的特点:

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数是自身类类型的引用,任何额外的参数都要有默认值(缺省值)
  • 自定义类型的传值传参和传值返回都会调用拷贝构造完成
  • 未显式定义时,编译器会自动生成拷贝构造函数,并且是浅拷贝(类似memcpy)

编译器生成的拷贝构造函数对内置类型成员会完成值拷贝/浅拷贝

编译器生成的拷贝构造函数对自定义类型成员会调用它的拷贝构造函数进行深拷贝

拷贝构造函数的使用规则:

如果类中没有申请内存资源时,可以不用显式写拷贝构造函数,直接用编译器生成的,如:Date

如果有资源申请,若使用编译器生成的拷贝构造函数会发生报错

需要手动实现深拷贝(对指向的资源也进行拷贝), 如:Stack

示例:

Stack类的_a指针指向了一定的空间资源,如果直接用编译器生成的拷贝构造函数

虽然能进行拷贝,但只是值拷贝,即将st1的指针变量_a中的存放的地址拷贝给了

st2的指针变量_a,导致了st1与st2里面的_a指针变量指向同一块空间

(注:指针变量是变量,是用来存放地址的变量)

在对对象st1和对象st2析构的时候, 编译器会对这块空间析构两次,造成程序崩溃

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	// Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
	// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
	Stack st2(st1);
	return 0;
}

所以需要手动实现拷贝构造函数,实现深拷贝

在深拷贝中,手动给st2开辟一块独立的空间

使用memcpy函数,复制st1指针指向的数据

这时st1与st2分别指向不同的内存空间,就不会发生错误

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	Stack(const Stack& st)
	{
		// 需要对_a指向资源创建同样⼤的资源再拷⻉值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	Stack st2(st1);
	return 0;
}

如果类的内部自定义成员有深拷贝函数,那么编译器自动生成的拷贝构造会调用该拷贝构造函数

也不需要手动实现,直接用编译器生成的,如:MyQueue

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	Stack(const Stack& st)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq1;
	MyQueue mq2 = mq1;
	return 0;
}

总结:

内置类型(int,double...):栈区,生命周期结束后,系统自动回收,只要浅拷贝

自定义类型(malloc,文件句柄,new...):堆区,需要手动释放,必须深拷贝

小技巧:

如果一个类显式实现了析构并且释放资源,那么它就需要写拷贝构造函数,否则就不需要

普通的构造函数:

这里的构造函数是全缺省构造函数,属于无参函数,虽然能够完成拷贝功能,但是不是拷贝构造,

只是一个普通的构造函数,拷贝构造函数必须有一个参数,并且是自身类类型的引用

传值传参的拷贝构造的问题:

C++规定传值传参时,必须要把实参拷贝一份,生成形参

如果实参是对象,则必须调用拷贝构造函数,但会发生无穷递归的问题

调用拷贝构造函数之前需要传值传参

而传值传参之前又需要拷贝一份实参

类比:

要求复印一份文件(调用拷贝构造函数)

但复印机要求先把原件复印一份(拷贝实参)

所以再去复印一份(重复调用拷贝函数)

但复印机要求先把原件复印一份(拷贝实参)...

解决方案:

使用引用传参,就不需要在传参的时候对实参进行拷贝

而是直接将参数传给拷贝构造函数后进行拷贝

类比:

要求复印一份文件(调用拷贝构造函数)

复印机直接帮你复印

**注:**引用前最好用const修饰,避免在拷贝构造函数体内实参值被修改

拷贝构造的两种写法:

cpp 复制代码
Stack st2(st1);
Stack st3 = st1;

5.6.运算符重载

运算符重载的概念:

具有特殊名字的函数,由operator和后面要定义的运算符构成

具有返回类型、参数列表、函数体

当运算符被用于类类型的对象时,C++可以通过运算符重载指定新的含义

运算符重载的特点:

  • 类类型对象使用运算符时,必须转换成调用对应运算符重载,如果没有则会编译报错
  • 重载运算符函数的参数个数与该运算符作用的运算对象个数一样多
  • 不能创建新的操作符,如:operator@
  • .*(成员函数回调),.,::,?:,sizeof,这5个运算符不能被重载
  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
  • 重载++运算符时,C++规定后置++增加int形参来与前置++构成函数重载,方便区分
  • 重载<<和>>时,需要重载为全局函数,因为重载为成员函数时,this指针默认抢占了第一个形参的位置。第一个形参位置为左侧运算对象,调用时就变成对象<<cout,不符合使用习惯,重载为全局函数,并且把ostream/istream放在第一个形参位置,类类型对象放在第二个形参位置

补充:

.*运算符

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
};

typedef void(A::* PF)();

int main()
{
    //等价于void(A::*pf)() = nullptr;
	PF pf = nullptr;

	//C++规定成员函数要加&才能取到函数指针
	pf = &A::func;

	A aa;
	(aa.*pf)();
}

示例:

比较自定义类型的大小,比如:日期(年,月,日,时,分,秒)

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
//private:
	int _year;
	int _month;
	int _day;
};

bool operator==(Date d1, Date d2)
{
	return d1._year == d2._year && d1._month == d2._month && d2._day == d1._day;
}

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2024, 7, 10);
	operator==(x1, x2);
	//等价于:x1 == x2;
	return 0;
}

可以将重载运算符函数放入类中当作成员函数

此时它的第一个运算符对象默认传给隐式的this指针,参数比运算对象少一个

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	bool operator==(Date d2)
	{
		return _year == d2._year && _month == d2._month &&_day == d1._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2024, 7, 10);
	x1.operator==(x2);
	//等价于:x1 == x2;

	return 0;
}

实现日期加减天数,日期减去日期

cpp 复制代码
//d1 + 100
Date operator+(int day);

//d1 - 100
Date operator+(int day);

//d1 - d2
int operator-(const Date& d);

注:

流插入与流删除不用写内置类型的数据类型是因为使用了运算符重载

对于自定义类型的流插入与流删除,需要我们手动写运算符重载函数

5.7.赋值运算符重载

**概念:**一个运算符重载

**作用:**完成两个已经存在的对象直接的拷贝复制

**区分:**拷贝构造用于一个对象拷贝初始化给另一个要创建的对象

cpp 复制代码
int main()
{
	Date x1(2024, 7, 10);
	Date x2(2024, 7, 10);
	
	//赋值重载拷贝
	x1 = x2;

	//拷贝构造
	Date x3(x2);
	Date x4 = x2;

	return 0;
}

赋值运算符重载的特点:

必须重载为成员函数,参数建议写成const+类类型引用,避免传值拷贝

示例:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2026, 2, 23);
	
	//赋值重载拷贝
	x1 = x2;

	x1.Print();
	x2.Print();
	return 0;
}

如果是值传参,类中的赋值重载函数作为成员函数,会调用拷贝构造函数

这里不会出现无穷递归问题,正常拷贝后返回,但最好使用引用,减少拷贝

cpp 复制代码
void operator=(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
cpp 复制代码
void operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

总结:

拷贝构造函数+传值传参 -> 无穷递归

其他成员函数+传值传参 -> 调用一次拷贝构造

引用传参 -> 不会出现递归

为了支持连续赋值,需要写返回值,并且建议写成当前类类型引用,避免拷贝,提高效率

示例:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2026, 2, 23);
	Date x3(2026, 2, 24);

	//连续赋值拷贝
	x1 = x2 = x3;

	x1.Print();
	x2.Print();
	x3.Print();
	return 0;
}

**注:**可以用指针,但是比引用麻烦

赋值重载:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	Date* operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return this;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2026, 2, 23);
	Date x3(2026, 2, 24);

	//连续赋值拷贝
	x1 = *(x2 = x3);

	x1.Print();
	x2.Print();
	x3.Print();
	return 0;
}

指针构造:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	Date(const Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2026, 2, 23);

	Date x3(&x2);

	x1.Print();
	x2.Print();
	x3.Print();
	return 0;
}

拷贝构造:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date x1(2024, 7, 10);
	Date x2(2026, 2, 23);

	Date x3(x2);

	x1.Print();
	x2.Print();
	x3.Print();
	return 0;
}

5.8.const成员函数

**概念:**const修饰的成员函数,放到成员函数列表的后面

const实际修饰该成员函数隐式的this指针

表明在该成员函数中不能对类的任何成员进行修改

示例:

用const修饰Date类的Print成员函数,Print隐含的this指针

由Date* const this 变为 const Date* const this

cpp 复制代码
// void Print(const Date* const this) const
void Print() const
{
    cout << _year << "-" << _month << "-" << _day << endl;
}

5.9.取地址运算符重载

分为普通取地址运算符重载和const取地址运算符重载

编译器默认形成的就足够使用,不需要显式实现

除非不想让别人取到当前类的对象的地址,可以自己实现一份,胡乱返回一个地址

cpp 复制代码
class Date
{
public:
	Date * operator&()
	{
		return this;
		// return nullptr;
	}
	const Date * operator&()const
	{
		return this;
		// return nullptr;
	}
private:
	int _year; // 年
	int _month; // ⽉
	int _day; // ⽇
};

六、日期类的实现

6.1.头文件编写

cpp 复制代码
#pragma once
#include<assert.h>
#include<iostream>
using namespace std;

class Date
{	
    //友元函数声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);

public:
	Date(int year = 1900, int month = 1, int day = 1);
	bool CheckDate();
	void Print();
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
		{
			return 29;
		}
		else
		{
			return monthDayArray[month];
		}
	}

	bool operator<(const Date& d) const; 
	bool operator<=(const Date& d) const;

	bool operator>(const Date& d) const;
	bool operator>=(const Date& d) const;

	bool operator==(const Date& d) const;
	bool operator!=(const Date& d) const;

	Date& operator+=(int day);
	Date operator+(int day) const;

	Date& operator-=(int day);
	Date operator-(int day) const;

	//后置++
	Date operator++(int);
	//前置++
	Date& operator++();
	//后置--
	Date operator--(int);
	//前置--
	Date& operator--();

	int operator-(const Date& d)  const;
	
private:
	int _year;
	int _month;
	int _day;
};

6.2.源文件编写

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"

void Date::Print()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

bool Date::CheckDate()
{
	if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
	{
		return false;
	}
	else
	{
		return true;
	}
}

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
	if (!CheckDate())
	{
		cout << "非法日期" << endl;
		Print();
	}
}

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

Date Date::operator+(int day) const
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

bool Date::operator<(const Date& d) const
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			if (_day == d._day)
			{
				return true;
			}
		}
	}
	return false;
}

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

bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}

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

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

//后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

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

int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n * flag;
}

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;
		if (!d.CheckDate())
		{
			cout << "输入日期非法:";
			d.Print();
			cout << "请重新输入!" << endl;
		}
		else
		{
			break;
		}
	}
	return in;
}

void TestDate1()
{
	Date d1(2026, 2, 24);
	Date d2 = d1 + 100;
	d1 += 100;

	d1.Print();
	d2.Print();
}

void TestDate2()
{
	Date d1(2026, 2, 24);
	Date d2 = d1 - 100;
	d1 -= 100;

	d1.Print();
	d2.Print();
}

void TestDate3()
{
	Date d1(2026, 2, 24);
	Date ret1 = ++d1;

	d1.Print();
	ret1.Print();

	Date d2(2026, 2, 24);
	Date ret2 = d2++;

	d2.Print();
	ret2.Print();
}

void TestDate4()
{
	Date d1(2026, 2, 24);
	d1 += -100;
	d1.Print();

	Date d2(2026, 2, 24);
	d2 -= -100;
	d2.Print();
}

void TestDate5()
{
	Date d1(2026, 2, 24);
	Date d2(2026, 2, 11);
	cout << d2 - d1 << endl;
}

void TestDate6()
{
	Date d1, d2;
	cout << d1 << d2;
}

void TestDate7()
{
	Date d1, d2;
	cin >> d1 >> d2;
	cout << d1 << d2;
	cout << d1 - d2 << endl;
}

int main()
{
	//TestDate1();
	//TestDate2();
	//TestDate3();
	//TestDate4();
	//TestDate5();
	//TestDate6();
	TestDate7();
	return 0;
}

七、再探构造函数

实现构造函数初始化成员变量有两种方式

**方法1:**函数体内赋值

**方法2:**初始化列表

7.1.初始化列表

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

cpp 复制代码
class Date
{
public://成员变量定义
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
    //成员变量声明
	int _year;
	int _month;
	int _day;
};

int main()
{
    //对象定义
    Date d1(2026,2,24);
    d1.Print();

    return 0;
}

每个成员变量在初始化列表中只能出现一次

**注:**const修饰的变量和引用变量必须在定义的时候给初值

所以const修饰的成员与引用成员都必须在初始化列表中初始化

cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int& xx,int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
		,_n(1)
		,_ref(xx)
	{
	}
private:
	int _year;
	int _month;
	int _day;
	const int _n;
	int& _ref;
};

int main()
{
	int x = 0;
	Date d1(x, 2026, 2, 25);

	return 0;
}

如果自定义类型有默认构造函数,初始化列表会自动调用它的默认构造函数

cpp 复制代码
#include <iostream>
using namespace std;

class Time
{
public:
	Time(int hour = 0)//默认构造
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int& xx,int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
		,_n(1)
		,_ref(xx)
	{
	}
private:
	int _year;
	int _month;
	int _day;
	const int _n;
	int& _ref;
	Time _t;
};

int main()
{
	int x = 0;
	Date d1(x, 2026, 2, 25);

	return 0;
}

如果自定义类型没有默认构造函数,那么就要在初始化列表中传参构造

cpp 复制代码
#include <iostream>
using namespace std;

class Time
{
public:
	Time(int hour)//普通构造
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int& xx,int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
		,_n(1)
		,_ref(xx)
		,_t(1)
	{
	}
private:
	int _year;
	int _month;
	int _day;
	const int _n;
	int& _ref;
	Time _t;
};

int main()
{
	int x = 0;
	Date d1(x, 2026, 2, 25);

	return 0;
}

总结:

引用成员变量const成员变量没有默认构造的类类型变量,必须在初始化列表位置进行初始化,否则编译报错

初始化列表和函数体可以配合使用

cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int& xx,int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
		,_n(1)
		,_ref(xx)
		,_ptr((int*)malloc(12))
	{
		if (_ptr == nullptr)
		{
			perror("malloc fail");
		}
		else
		{
			memset(_ptr, 0, 1);
		}
	}
private:
	int _year;
	int _month;
	int _day;
	const int _n;
	int& _ref;
	int* _ptr;
};

int main()
{
	int x = 0;
	Date d1(x, 2026, 2, 25);

	return 0;
}

补充:

在C++11中,可以在成员变量声明时赋缺省值,在初始化列表时使用(这里并非定义)

cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
	}
private:
	//声明,缺省值是在初始化列表使用的
	int _year = 1;
	int _month = 1;
	int _day = 1;
    //缺省值也可以是表达式
    int* _ptr = (int*)malloc(12);
};

int main()
{
	int x = 0;
	Date d1(2026, 2, 25);

	return 0;
}

总结:

每个成员都要通过初始化列表

一、在初始化列表初始化的成员

二、没有在初始化列表初始化的成员

1.声明的地方有缺省值就用缺省值初始化

2.声明的地方没有缺省值

如果是内置类型:编译器决定,大概率为随机值

如果是自定义类型:调用默认构造函数,无默认构造函数编译报错

三、引用,const修饰,无默认构造函数的自定义类型必须在初始化列表初始化

初始化列表中按照成员变量在类中的声明顺序进行初始化

与成员在初始化列表出现的先后顺序无关

示例:

先初始化_a2,此时_a1虽然有缺省值,但还未初始化,所以_a2被初始化为随机值

然后再用a初始化_a1

cpp 复制代码
#include<iostream>
using namespace std;

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

int main()
{
	A aa(1);
	aa.Print();
}

八、类型转换

8.1.隐式类型转换

C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数

构造函数前加explicit就不再支持隐式类型转换

类类型的对象之间也可以隐式转换,需要相应构造函数支持

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	// 构造函数explicit就不再支持隐式类型转换
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{
	}
	//explicit A(int a1, int a2)
	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{
	}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
	int Get() const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
public:
	B(const A& a)
		:_b(a.Get())
	{
	}
private:
	int _b = 0;
};

class Stack
{
public:
    void Push(const A& aa)
    {

    }
private:
    A _arr[10];
    int _top;
}


int main()
{
	//用1构造⼀个A的临时对象,再用这个临时对象拷⻉构造aa1
	//编译器遇到连续构造+拷⻉构造会优化为直接构造
	A aa1 = 1;
	aa1.Print();

    //临时变量具有常性,需要加const
	const A& aa2 = 1;

	// C++11之后才支持多参数转化
	A aa3 = { 2,2 };

	// aa3隐式类型转换为b对象
	// 原理跟上面类似
	B b = aa3;
	const B& rb = aa3;

    Stack st;

    A aa(3);
    st.Push(aa3);

    st.Push(3);

	return 0;
}

九、static成员

9.1.静态成员变量的概念

用static修饰的成员变量

9.2.静态成员变量的特点

静态成员变量一定要在类外进行初始化

静态成员函数为当前类的所有对象共享,不属于某个具体对象,存放在静态区

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
private:
	//类里面声明
	static int _scount;
};

// 类外面初始化
int A::_scount = 0;

int main() 
{
	cout << sizeof(A) << endl;
	return 0;
}

9.3.静态成员函数的概念

用static修饰的成员函数

9.4.静态成员函数的特点

静态成员函数没有this指针

cpp 复制代码
// 实现⼀个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
	static int GetACount()
	{
		return _scount;
	}
private:
	//类里面声明
	static int _scount;
};

// 类外面初始化
int A::_scount = 0;

int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
    //出了代码段,a3就销毁了
    {
	    A a3(a1);
	    cout << A::GetACount() << endl;
    }
    cout << A::GetACount() << endl;
		
	return 0;
}

静态成员函数中可以访问静态成员,但不能访问非静态成员,因为没有this指针

非静态的成员函数可以访问任意的静态函数和非静态函数

可以通过类名::静态成员,或者对象.静态成员来访问静态成员变量和静态成员函数

试题1:求1+2+3+...+n

题目内容:

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

示例:

输入:5

输出:15

cpp 复制代码
#include <regex>

class Sum
{
public:
    Sum()
    {
        _ret += _i;
        ++_i;
    }
    static int GetRet()
    {
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};

int Sum::_i = 1;
int Sum::_ret = 0;

class Solution
{
public:
    int Sum_Solution(int n) 
    {
        //C99后支持变长数组
        Sum a[n];
        return Sum::GetRet();
    }
};

试题2:

构造函数的调用顺序为E

全局变量在main函数之前创建,所以c先初始化

静态成员d在a,b之后初始化

析构函数的调用顺序为B

局部变量先析构,后定义的先析构,b在a之前

生命周期为全局,局部静态先析构,d在c之前

十、友元

10.1.友元的概念

一种突破类访问限定符封装的方式,分为友元函数和友元类

10.2.友元的实现

在函数声明或者类声明的前面加friend,并且把友元声明放到一个类里面

10.3.友元函数的特点

外部友元函数可以访问类的私有和保护成员

友元函数只是一种声明,而非类的成员函数(没有this指针)

友元函数可以在类定义的任何地方声明

一个函数可以是多个类的友元函数

cpp 复制代码
#include<iostream>
using namespace std; 

//前置声明:否则A的友元函数声明时,编译器不认识B
class B;

class A
{
	//友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
	//友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};

void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}

int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

友元类的成员函数都可以是另一个类的友元函数

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
	// 友元声明
	friend class B;
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
public:
	void func1(const A & aa)
	{
		cout << aa._a1 << endl;
		cout << _b1 << endl;
	}
	
	void func2(const A & aa)
	{
		cout << aa._a2 << endl;
		cout << _b2 << endl;
	}
private:
	int _b1 = 3;
	int _b2 = 4;
};

int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func1(aa);
	return 0;
}

友元类的关系是单向的,不具有交换性

A类是B类的友元,但B类不是A类的友元

友元类的关系不能传递

A类是B类的友元,B类是C类的友元,但A类不是C类的友元

友元虽然提供便利,但是会增加耦合度,破坏了封装

十一、内部类

11.1.内部类的概念

一个类定义在另一个类的内部

内部类是一个独立的类,与定义在全局相比

它只受到外部类类域限制和访问限定符限制

所以外部类定义的对象中不包含内部类

11.2.内部类的特点

内部类默认是外部类的友元类

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	class B//B默认是A的友元类
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl;
			cout << a._h << endl;
		}
		int _b1;
	};
private:
	static int _k;
	int _h = 1;
};

int main()
{
	cout << sizeof(A) << endl; 
	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}

11.3.内部类的本质

一种封装,当A类与B类紧密关联

A类实现出来主要就是给B类使用

可以考虑把A类设计为B的内部类

如果放到private或protected位置

那么A类就是B类的专属内部类,其他地方用不了

cpp 复制代码
#include <regex>

class Solution 
{
    // 内部类
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }   
    };

    static int _i;
    static int _ret;

public:
    int Sum_Solution(int n)
    {
        // 变⻓数组
        Sum arr[n];
        return _ret;
    }
};

int Solution::_i = 1;
int Solution::_ret = 0;

十二、匿名对象

12.1.匿名对象的概念

用类型定义出来的对象叫做匿名对象

用类型对象名定义出来的叫有名对象

12.2.匿名对象的特点

匿名对象的生命周期只在当前一行

一般临时定义一个对象当前用一下

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A(int a)" << endl;
	}
	
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

class Solution 
{
public:
	int Sum_Solution(int n) 
	{
		//...
		return n;
	}
};

int main()
{
    //有名对象
	A aa1;

	// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
	//A aa1();

    //匿名对象
	//⽣命周期只有这⼀⾏,下⼀⾏会自动调用析构函数
	A();
	A(1);
	
	A aa2(2);
	
	//匿名对象在这样场景下就很好⽤
	Solution().Sum_Solution(10);
	
	return 0;
}

十三、对象拷贝时的编译器优化

现代编译器会为了提高程序的效率

会尽可能减少传参和传返回值过程中的拷贝

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

void f1(A aa)
{}

A f2()
{
	 A aa;
	 return aa;
}

int main()
{
	//传值传参
	//构造+拷贝构造
	A aa1;
	f1(aa1);
	cout << endl;

	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	
	//一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;

	cout << "***********************************************" << endl;

	//传值返回
	//不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值

	//无优化 (vs2019 debug)
	//一些编译器会优化得更厉害,将构造的局部对象和拷⻉构造的临时对象优化为直接构造(vs2022 debug)
	f2();
	cout << endl;

	//返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
	//一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造(vs2022 debug)
	A aa2 = f2();
	cout << endl;

	//一个表达式中,开始构造,中间拷贝构造+赋值重载->无法优化(vs2019 debug)
	//一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝临时对象合并为一个直接构造(vs2022 debug)
	aa1 = f2();
	cout << endl;
	
	return 0;
}
相关推荐
十五年专注C++开发1 小时前
Qt deleteLater作用及源码分析
开发语言·c++·qt·qobject
稻草猫.1 小时前
TCP与UDP:传输层协议深度解析
笔记·后端·网络协议
阿闽ooo2 小时前
中介者模式打造多人聊天室系统
c++·设计模式·中介者模式
Sunsets_Red2 小时前
P8277 [USACO22OPEN] Up Down Subsequence P 题解
c语言·c++·算法·c#·学习方法·洛谷·信息学竞赛
汉克老师2 小时前
GESP2023年12月认证C++二级( 第三部分编程题(2) 小杨的H字矩阵)
c++·算法·矩阵·循环结构·gesp二级·gesp2级
奶茶树2 小时前
【数据结构】红黑树
数据结构·c++·算法
Once_day2 小时前
GCC编译(7)链接脚本LinkerScripts
c语言·c++·编译和链接·程序员自我修养
问好眼2 小时前
《算法竞赛进阶指南》0x01 位运算-2.增加模数
c++·算法·位运算·信息学奥赛
蒸蒸yyyyzwd2 小时前
redis实战学习笔记p1-12
数据库·笔记