C++初阶—类与对象(中篇)

第一章:类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

第二章:构造函数 

2.1 概念

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;
	d1.Init(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法初始化,用完后还要销毁。可能会忘记这些步骤。所以引出构造函数。

构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

2.2 特性

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

其特征如下:

1. 函数名与类名相同。

2. 无返回值。(不需要写void)

3. 对象实例化时编译器自动调用对应的构造函数。

class Stack { 
public:
	//void Init() { 
	//	a = nullptr;
	//	top = capacity = 0;
	//}

	//如果按下方这种方式初始化,不能完全满足所有需求。
	//如果要向栈里插入1000个元素,就需要多次扩容。(如果是异地扩容,开销很大,因为需要拷贝数据
	//Stack() { //构造函数
	//	a = nullptr;
	//	top = capacity = 0;
	//}

	//优化版本
	Stack(size_t n = 4) {
		// n 为 0,1.避免无效内存分配;
		//2.防止错误状态:初始化 a 为 nullptr 和将 top、capacity 设置为 0,
		//确保对象处于有效的初始状态,即使不分配内存也不会导致未定义的行为或内存访问错误。
		if (n == 0) {
			a = nullptr;
			top = capacity = 0;
		}
		else {
			a = (int*)malloc(sizeof(int) * n);
			if (a == nullptr) {
				perror("malloc a fail");
				exit(-1);
			}
			top = 0;
			capacity = n;
		}
	}

	void Push(int x) {
		if (top == capacity) {	
			int newcapacity = capacity == 0 ? 4 : capacity * 2;
			int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
			if (!tmp) {
				perror("realloc int* tmp fail");
				exit(-1);
			}

			if (tmp == a)
				cout << capacity << "原地扩容" << endl;//打印
			else
				cout << capacity << "异地扩容" << endl;//打印

			a = tmp;
			capacity = newcapacity;
		}
		a[top++] = x;
	}

	int Top() {
		assert(top > 0);
		return a[top - 1];
	}

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

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

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

private: 
	int* a;
	int top;
	int capacity;
};

int main() {
	//栈1
	Stack st1;
	
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);

	while (!st1.Empty()) {
		cout << st1.Top() << " ";
		st1.Pop();
	}
	cout << endl;
	st1.Destroy();

	//栈2
	//Stack st2;//如果不设计全缺省的构造函数,就需要多次扩容
	Stack st2(1000);
	for (size_t i = 0; i < 1000; i++) {
		st2.Push(i);
	}

	while (!st2.Empty()) {
		cout << st2.Top() << " ";
		st2.Pop();
	}
	cout << endl;
	st2.Destroy();
}

4. 构造函数可以重载。(可以写多个构造函数,提供多种初始化方式)

class Date {
public:
	// 1.无参构造函数
	Date() {
		_year = 1;
		_month = 1;
		_day = 1;
	}

	// 2.带参构造函数
	Date(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}

	//3.合并上方两种写法(全缺省参数)。并且更灵活
	//1和3可以同时存在,构成函数重载,但是调用存在问题。如果不传参,编译器不知道调用的是1还是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 d1; // 调用无参构造函数
	d1.Print();
	Date d2(2024, 8, 12); // 调用带参的构造函数
	d2.Print();
	Date d3(2024, 8);//全缺省演示
	d3.Print();
	Date d4(2024);//全缺省演示
	d4.Print();

	//Date d3();//错误演示1。如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	//Date d5();//错误演示2。虽然是全缺省构造函数,但不传参对象后面不用跟括号,否则就成了函数声明
	//d5.Print();
	
	return 0;
}

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

class Date {
public:
	 如果用户显式定义了构造函数,编译器将不再生成
	//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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
	// 放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
	Date d1;
	return 0;
}

6. 关于编译器生成的默认成员函数,会产生疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认构造函数。

class Time {
public:
	Time() {
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date {
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};

int main() {
	Date d;//Time()
	return 0;
}

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在类中声明时可以给默认值。
class Time {
public:
	Time() {
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date {
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

int main() {
	Date d;
	return 0;
}

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

不传参就可以调用的就是默认构造函数,三个只能存在一个,否则调用存在歧义

编译器生成的默认构造函数的特点:

1.如果不写才会生成,否则写了任意一个构造函数就不会生成了
class Date {
public:
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main() {
	//如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,
	//一旦用户显式定义编译器将不再生成。
	
	//示例1:没有显示定义,编译器自动生成无参构造函数
	Date d1;//这里生成了构造函数,也可以调用。但没初始化。
	d1.Print();//打印时都是随机值。因为内置类型成员不处理初始化。C++11,声明支持给缺省值
    
    return 0;
}

2.内置类型的成员(即int、char之类关键字定义)不会处理(C++11,声明支持给缺省值)
//示例1
class Date {
public:
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//内置类型的成员(即int、char之类关键字定义)不会处理(C++11,声明支持给缺省值)
	//但注意不要重复初始化,如果自己定义了构造函数且初始化就不要在内置类型声明时给缺省值
	int _year = 1;//声明给的缺省值
	int _month = 1;
	int _day = 1;
};

int main() {
	Date d1;// 1/1/1
	d1.Print();

	return 0;
}


//示例2
class Date {
public:
	Date() {
		//这里没有初始化year,所以year就是内置类型声明时的1
		_month = 2;
		_day = 2;
	}

	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year = 1;
	int _month;
	int _day;
};

int main() {
	Date d1;// 1/2/2
	d1.Print();

	return 0;
}

3.自定义类型的成员(即类、结构体)才会处理,会去调用这个成员的默认构造函数
class Stack {
public:
	Stack(size_t n = 4) {
		cout << "Stack(size_t n = 4)" << endl;//为了验证MyQueue这个类,编译器自动生成的构造函数
		if (n == 0) {
			a = nullptr;
			top = capacity = 0;
		}
		else {
			a = (int*)malloc(sizeof(int) * n);
			if (a == nullptr) {
				perror("malloc a fail");
				exit(-1);
			}
			top = 0;
			capacity = n;
		}
	}

	void Push(int x) {
		if (top == capacity) {

			int newcapacity = capacity == 0 ? 4 : capacity * 2;
			int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
			if (!tmp) {
				perror("realloc int* tmp fail");
				exit(-1);
			}

			if (tmp == a)
				cout << capacity << "原地扩容" << endl;//打印
			else
				cout << capacity << "异地扩容" << endl;//打印

			a = tmp;
			capacity = newcapacity;
		}
		a[top++] = x;
	}

	int Top() {
		assert(top > 0);
		return a[top - 1];
	}

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

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

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

private:
	int* a;
	int top;
	int capacity;
};

//两个栈实现一个队列
class MyQueue {
private:
	Stack _pushst;
	Stack _popst;
};

int main() {
	//示例3:自定义类型的成员(即类、结构体)才会处理,会去调用这个成员的构造函数
	MyQueue mq;//mq不需要写构造函数。编译器会生成默认构造函数,该构造函数会使用Stack的构造函数
	
    return 0;
}

总结:一般情况下都需要我们自己写构造函数,决定初始化方式。

第三章:析构函数

3.1 概念

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

3.2 特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统自动调用析构函数。
  5. 编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
  6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

示例1

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

	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	~Date() {
		//这里只是为了证明自动调用了析构函数。
		//这个类并没有要清理的资源
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d;
	return 0;
}

示例2

class Stack {
public:
	Stack(size_t n = 4) {
		//cout << "Stack(size_t n = 4)" << endl;//为了验证MyQueue这个类,编译器自动生成的构造函数
		if (n == 0) {
			a = nullptr;
			top = capacity = 0;
		}
		else {
			a = (int*)malloc(sizeof(int) * n);
			if (a == nullptr) {
				perror("malloc a fail");
				exit(-1);
			}
			top = 0;
			capacity = n;
		}
	}

	void Push(int x) {
		if (top == capacity) {

			int newcapacity = capacity == 0 ? 4 : capacity * 2;
			int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
			if (!tmp) {
				perror("realloc int* tmp fail");
				exit(-1);
			}

			if (tmp == a)
				cout << capacity << "原地扩容" << endl;//打印
			else
				cout << capacity << "异地扩容" << endl;//打印

			a = tmp;
			capacity = newcapacity;
		}
		a[top++] = x;
	}

	int Top() {
		assert(top > 0);
		return a[top - 1];
	}

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

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

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

	~Stack() {
		//cout << "~Stack()" << endl;//为了验证自动调用析构函数
		free(a);
		a = nullptr;
		top = capacity = 0;
	}

private:
	int* a;
	int top;
	int capacity;
};

int main() {
	//后定义,先析构
	Stack st1;
	Stack st2;
	因为在栈帧里面,后进先出。所以调用的第一个析构是st2

	return 0;
}

有了构造函数和析构函数可以重新对括号匹配问题进行改写

1. 创建并初始化栈
2. 数组指针遇到左括号入栈
3. 数组指针遇到右括号份2种情况:
    a. 栈为空,返回假。(说明之前没有遇到左括号)
    b. 栈不为空,获取栈顶元素与数组指针指向元素比较是否匹配。(注意匹配条件,找为假的情况)
重复上述步骤直到遍历完数组。
4.最后判断栈是否为空。空为真,非空为假。
 

C语言版本

bool isValid(char* s) {
    //1.创建并初始化栈
    ST st;
    STInit(&st);

    //2.遍历字符串
    while (*s) {
        if (*s == '(' || *s == '[' || *s == '{') //1.如果是左括号就入栈
            STPush(&st, *s);
        else { //2.不是左括号就出栈
            if (STEmpty(&st)) { //2.1 出栈前先看栈是否为空,为空说明没有左括号,直接返回假
                STDestroy(&st);
                return false;
            }
            int top = STTop(&st); //获取栈顶元素
            STPop(&st);
            //2.2 如果栈顶元素的左括号和字符串指针指向的右括号不匹配则返回假
            if ((top == '(' && *s != ')') || (top == '[' && *s != ']') || (top == '{' && *s != '}'))
                return false;       
        }
        s++;
    }
    bool ret = STEmpty(&st);//遍历完后还需看栈是否为空,为空说明还有单独的左括号,不匹配
    STDestroy(&st);
    return ret;
}

C++版本

bool isValid(const char* s) {
	Stack st;// 创建栈       

	// 1.左括号入栈
	// 2.出栈元素与右括号匹配
	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 == '{'))
			//这里不能用上方条件判断正确后直接返回true,因为最后一个元素为左括号时也满足,但不正确 
			//下方如果栈顶元素与数组指针指向元素不匹配返回假
			if ((*s == ')' && top != '(') || (*s == ']' && top != '[') || (*s == '}' && top != '{'))
				return false;
		}
		s++;
	}
	//这里判断栈里是否还有元素,
	// 如果有,说明有一个单独的左括号,不匹配
	// 如果没有,说明都匹配完成
	bool ret = st.Empty();
	return ret;
}

第四章:拷贝构造函数

4.1 概念

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

4.2 特征

拷贝构造函数也是特殊的成员函数,其特征如下:

1. 拷贝构造函数是构造函数的一个重载形式

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

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

	//1.传值调用:
	//当你调用 Date d2(d1); 时,d1 是实参,d2 是要被创建的新对象。
	//为了将 d1 传递给拷贝构造函数,编译器需要先创建一个 Date 对象作为 d 形参的临时拷贝。
	//2.创建临时拷贝:
	//在这个过程中,编译器会创建一个新的 Date 对象来作为 d 的值。
	//这个临时对象的创建是通过调用拷贝构造函数实现的,因为它需要拷贝 d1 对象。
	//3.触发拷贝构造函数:
	//为了创建这个临时拷贝对象,编译器会调用拷贝构造函数 Date(Date d),来初始化这个新的 Date 对象。
	//4.递归调用:
	//由于拷贝构造函数的参数类型是 Date 对象(而不是引用),
	//这个新的 Date 对象(即临时拷贝)也需要使用拷贝构造函数来完成初始化。
	//这导致了无限递归,因为拷贝构造函数会被不断调用来初始化新的临时拷贝对象。

	//因为是传值调用,且d1是实参,在传参时形参是实参的一份临时拷贝。
	//这里需要拷贝的是类的对象,而实现这个类对象拷贝的函数是拷贝构造函数。
	//根据上述所说传参时会发生实参的临时拷贝,也就是调用拷贝构造函数去拷贝d1。
	//因此造成了无限递归(如果参数是按值传递的(Date d),那么就会在调用拷贝构造函数时创建一个新的对象,这可能会导致无限递归。)
	//Date(Date d) { //错误示例
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}
	
    //Date d2(d1);//d是d1的别名,d1不能修改
	Date(const Date& d) { //正确版本
		//推荐加上const 防止写反(d._year = _year;)
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。 

class Time {
public:
	Time() {
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t) {
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date {
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

int main() {
	Date d1;

	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?

// 这里会发现下面的程序会崩溃掉,这里就需要后面讲的深拷贝去解决。
typedef int DataType;
class Stack {
public:
	Stack(size_t capacity = 10) {
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array) {
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data) {
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack() {
		if (_array) {
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};

int main() {
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 

栈类的正确版本

typedef int DataType;
class Stack {
public:
	Stack(size_t capacity = 4) {
		cout << "Stack(size_t capacity = 4)" << endl;
		if (capacity == 0) {
			_array = nullptr;
			_size = _capacity = 0;
		}
		else {
			_array = (int*)malloc(sizeof(int) * capacity);
			if (_array == nullptr) {
				perror("malloc a fail");
				exit(-1);
			}
			_size = 0;
			_capacity = capacity;
		}
	}

	Stack(const Stack& s) {
		cout << "Stack(Stack& s)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * s._capacity);//开辟新对象_array的空间
		if (_array == nullptr) {
			perror("malloc a fail");
			exit(-1);
		}
		//将要拷贝对象中的数据拷贝过来
		memcpy(_array, s._array, sizeof(DataType) * s._size);
		_size = s._size;
		_capacity = s._capacity;
	}

	void Push(DataType data) {
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	~Stack() {
		cout << "~Stack()" << endl;
		free(_array);
		_array = nullptr;
		_size = _capacity = 0;
	}
private:
	// 内置类型
	DataType* _array;
	int _capacity;
	int _size;
};

5. 拷贝构造函数典型调用场景:

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

第五章:赋值运算符重载

5.1 运算符重载

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

函数名字为:关键字operator后面接需要重载的运算符符号

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

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@ 
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*   ::   sizeof   ?:   .   注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

全局版

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 riqixiaoyu(const Date& x1, const Date& x2)
//bool Compare1(const Date& x1, const Date& x2)
//为了避免出现上面函数名不明确,且提高代码可读性(cout << d1 < d2 << endl;),所以引出了运算符重载
//编译器并不知道自定义类型怎么比较,所以需要自己实现(全局版)
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;
	else
		return false;
}

int main() {
	Date d1(2024, 8, 19);
	Date d2(2023, 7, 18);
	//d1 < d2;//不能用这个运算符比较日期大小。所以需要一个比较函数
	cout << (d1 < d2) << endl;//这里比较需要加括号,因为<<流运算符优先级更高
	cout << (operator<(d1, d2)) << endl;//显示调用。operator加<就是函数名
	return 0;
}

待解决问题:成员变量不在是私有
将比较函数改为成员函数

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<(const Date& x1, const Date& x2) { //错误版本
	//成员函数的签名中,运算符重载函数只有一个参数,因为左侧的对象隐式地作为 this 被传递。
	//这里的 operator< 函数有两个参数,这在成员函数中是不允许的。
	
	// bool operator<(Date* this, const Date& x2)
	// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator<(const Date& x2) {
		if (_year < x2._year)
			return true;
		else if (_year == x2._year && _month < x2._month)
			return true;
		else if (_year == x2._year && _month == x2._month && _day < x2._day)
			return true;
		else
			return false;
	}

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

int main() {
	Date d1(2024, 8, 19);
	Date d2(2023, 7, 18);
	cout << (d1 < d2) << endl;
	cout << (d1.operator<(d2)) << endl;

	return 0;
}

5.2 赋值运算符重载

1. 赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义
class Date {
public:
	Date(int year = 1900, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}

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

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

		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,所以像日期类这样的类是没必要的。

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

5.3 前置++和后置++重载

//前置++和后置++不仅是运算符重载,还构成函数重载

class Date {
public:
	Date(int year = 1900, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++() {
		_day += 1;
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
	// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
	Date operator++(int) {
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d;
	Date d1(2022, 1, 13);
	d = d1++;// d: 2022,1,13   d1:2022,1,14
	d = ++d1;// d: 2022,1,15   d1:2022,1,15
	return 0;
}

第六章:日期类的实现

Date.h

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;

class Date {
	//friend void operator<<(ostream& out, const Date& d);//通过友元声明让类外部函数可以访问内部私有成员变量
	friend ostream& operator<<(ostream& out, const Date& d);//支持连续流插入
	friend istream& operator>>(istream& in, Date& d);
	

public:
	//声明:在类的声明部分,提供默认参数是为了让编译器和使用者了解函数的接口和参数的默认值。
	//声明是在头文件中进行的,目的是让其他代码知道如何调用该函数。

	//缺省参数声明给,定义不给。因为.cpp文件包含的是.h的头文件
	//声明给了缺省参数,编译才能通过。链接时才找函数的地址
	int GetMonthDay(int year, int month);
	Date(int year = 1, int month = 1, int day = 1);

	//下方两个函数构成函数重载,可以同时存在。
	//但这里没有必要同时存在,没有意义
	//void Print();//此版本不能传const修饰的参数
	void Print() const;

	//Date d1(d3)
	//Date(const Date& d) {}

	//d1 = d3
	//如果不想改变实参,传引用时加const保护
	//void operator=(const Date& d) { //这种写法不支持连续赋值,因为没有返回值
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

	//Date operator=(const Date& d) { //如果是传值返回,会调用拷贝构造。又因为出了作用域*this还在。所以可以改为传引用返回
	//Date& operator=(const Date& d) {
	//	if (this != &d) { //加这个条件是为了防止自己给自己赋值
	//		_year = d._year;
	//		_month = d._month;
	//		_day = d._day;
	//	}
	//	return *this;
	//}

	Date& operator=(const Date& d);

	//总结:只读函数可以加const,内部不涉及修改成员
	bool operator<(const Date& x2) const;
	bool operator==(const Date& x2) const;
	bool operator<=(const Date& x2) const;
	bool operator>(const Date& x2) const;
	bool operator>=(const Date& x2) const;
	bool operator!=(const Date& x2) const;

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

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

	//前置++和后置++不仅是运算符重载,还构成函数重载
	//++d1 -> d1.operator++()
	Date& operator++();

	//d1++ -> d1.operator++(0)
	//加一个int参数,进行占位,跟前置++构成函数重载进行区分
	//本质后置++调用,编译器进行特殊处理
	Date operator++(int);

	Date& operator--();
	Date operator--(int);

	int operator-(const Date& d) const;//日期相减

	//const int fun() {}
	//这里const的作用是不能修改返回值。
	//但是传值返回 返回的是返回值的一份拷贝(临时对象),本身具有常性,所以这里的const没有意义
	//只有传引用返回的const才有意义

	//8.取地址及const取地址操作符重载
	//这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
	//这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,
	//只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
	
	//自定义分为两个版本,一个是非const版本,一个是const版本
	//Date* operator&() {
	//	return this;
	//}
	//const Date* operator&() const {
	//	return this;
	//}

	//void operator<<(ostream& out);//因为不符合使用习惯,所有不定义为成员函数

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

//调整为全局函数
//void operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d);//支持连续流插入
istream& operator>>(istream& in, Date& d);//这里的Date& d不能加const,因为从控制台提取数据后有可能要改变该内容

Date.cpp

#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"

//Date::部分是一个作用域解析符,它表示 Date 是一个类名,Date 构造函数属于 Date 类。
int Date::GetMonthDay(int year, int month) {
	//monthArray这个数组会大量重复调用,加static延长声明周期,避免重复创建
	//加const防止该数组值被修改
	const static int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	//要先判断是否为2月,否则每次都会先判是否为断闰年,没意义
	if ((month == 2) && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		return 29;
	return monthArray[month];//这里返回的是数组值的临时拷贝对象,本身具有常性
}

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

	//检查日期是否合法
	if (month < 1 || month>12 || day<1 || day>GetMonthDay(year, month))
		cout << "非法日期" << endl;
}

void Date::Print() const { //这里的const本质是void Date::Print(const Date* this) 不能修改this指向的对象
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

Date& Date::operator=(const Date& d) {
	if (this != &d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}
//d1 < d2
//bool Date::operator<(Date* this, const Date& x2)
bool Date::operator<(const Date& x2) const {
	if (_year < x2._year) {
		return true;
	}
	else if (_year == x2._year && _month < x2._month) {
		return true;
	}
	else if (_year == x2._year && _month == x2._month && _day < x2._day) {
		return true;
	}
	else {
		return false;
	}
}

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

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

bool Date::operator>(const Date& x2) const {
	return !(*this <= x2);//<=逻辑取反就是 >
}

bool Date::operator>=(const Date& x2) const {
	return !(*this < x2);//>=逻辑取反就是 <
}

bool Date::operator!=(const Date& x2) const {
	return !(*this == x2);//==逻辑取反就是 !=
}


加复用加等(该版本更好)。该版本未处理day为负数的情况
//Date& Date::operator+=(int day) { //出了该函数,对象还在。如果返回Date,会生成一份拷贝,拷贝又要调用拷贝构造。所以返回引用
//	//月进位
//	//先加天数,并判断是否需要进位
//	_day += day;//_day是目前天数,day是加上的天数
//	//如果目前天数大于目前年份月份天数,那么要将现在的天数减去该月天数,并且月份进位
//	//重复上述步骤,直到目前天数符合该月份要求
//	while (_day > GetMonthDay(_year, _month)) {
//		_day -= GetMonthDay(_year, _month);
//		_month++;
//		//年进位
//		if (_month == 13) {
//			_year++;
//			_month = 1;
//		}
//	}
//	return *this;//返回调用该函数的Date类的对象(this是指针)
//}

//可处理day为负数的版本
Date& Date::operator+=(int day) { //出了该函数,对象还在。如果返回Date,会生成一份拷贝,拷贝又要调用
	if (day < 0)
		return *this -= (-day);//加负的天数等于减正的天数

	//月进位
	//先加天数,并判断是否需要进位
	_day += day;//_day是目前天数,day是加上的天数
	//如果目前天数大于目前年份月份天数,那么要将现在的天数减去该月天数,并且月份进位
	//重复上述步骤,直到目前天数符合该月份要求
	while (_day > GetMonthDay(_year, _month)) {
		_day -= GetMonthDay(_year, _month);
		_month++;
		//年进位
		if (_month == 13) {
			_year++;
			_month = 1;
		}
	}
	return *this;//返回调用该函数的Date类的对象(this是指针)
}

Date Date::operator+(int day) const { //实现正确的日期+天数
	Date tmp(*this);//因为不能改变自己(即调用该函数的对象),所以要调用拷贝构造函数拷贝一份
	tmp += day;
	return tmp;//tmp出了该函数作用域就不在了,所以是传值返回
}

加等复用加
//Date Date::operator+(int day) { //实现正确的日期+天数
//	//因为不能改变自己(即调用该函数的对象),所以要调用拷贝构造函数拷贝一份
//	Date tmp(*this);
//	//Date tmp = *this;//等价上方,都是拷贝构造
//	tmp._day += day;
//	while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {
//		tmp._day -= GetMonthDay(tmp._year, tmp._month);
//		tmp._month++;
//		//年进位
//		if (tmp._month == 13) {
//			tmp._year++;
//			tmp._month = 1;
//		}
//	}
//	return tmp;//tmp出了该函数作用域就不在了,所以是传值返回
//}
//
//Date& Date::operator+=(int day) {
//	//Date ret = *this + day;
//	//*this = ret;
//	
//	//赋值重载版本
//	*this = *this + day;//只有赋值重载能够完成的场景
//	return *this;
//}
//总结:+复用+=更好。
//不管是哪一种,+的重载都拷贝了两次,第一次当前对象拷贝给tmp,第二次传值返回又拷贝一次。
//但是, +复用+=没有发生额外的拷贝;而+=复用+时,调用了+,上面提到+拷贝了两次。



该版本未处理负的天数
//Date& Date::operator-=(int day) {
//	_day -= day;//目前天数减 要减的天数
//	while (_day <= 0) { //减完后,目前天数<=0都不对,需要继续处理。借位
//		--_month;//目前天数<=0,要向月份借
//		if (_month == 0) { //如果月份都为0,就要像年借
//			--_year;
//			_month = 12;
//		}
//		//目前天数向月份和年份借一次后,年份和月份也可能发生变化。
//		//将变化后的年月的天数加到剩余的目前天数。如果天数合法,循环结束;如果天数依然为负,那么继续借。
//		_day += GetMonthDay(_year, _month);
//	}
//	return *this;
//}

//可处理负的天数版本
Date& Date::operator-=(int day) {
	if (day < 0)
		return *this += (-day);//减负的天数等于加正的天数

	_day -= day;//目前天数减 要减的天数
	while (_day <= 0) { //减完后,目前天数<=0都不对,需要继续处理。借位
		--_month;//目前天数<=0,要向月份借
		if (_month == 0) { //如果月份都为0,就要像年借
			--_year;
			_month = 12;
		}
		//目前天数向月份和年份借一次后,年份和月份也可能发生变化。
		//将变化后的年月的天数加到剩余的目前天数。如果天数合法,循环结束;如果天数依然为负,那么继续借。
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

Date Date::operator-(int day) const { //日期减天数
	Date tmp(*this);
	tmp -= day;
	return tmp;
}


//++d1
Date& Date::operator++() {
	*this += 1;
	return *this;//前置++,返回++以后的值
	//this指向对象出了该函数还在,所以传引用返回
}

//d1++
Date Date::operator++(int) {
	Date tmp(*this);
	*this += 1;
	return tmp;//后置++,返回++前的值
	//tmp出了该函数不在,所以传值返回
}
//无论是前置++还是后置++,自身都要++,只是返回的值不一样


Date& Date::operator--() {
	*this -= 1;
	return *this;
}
Date Date::operator--(int) {
	Date tmp(*this);
	*this -= 1;
	return tmp;
}


int Date::operator-(const Date& d) const { //日期相减
	Date max = *this;
	Date min = d;
	int flag = 1;//this是左,d是右。左大右小减出来结果是正的

	if (*this < d) {
		min = *this;
		max = d;
		flag = -1;
	}

	int n = 0;
	while (min != max) { //这里也可以用min < max,不过!=的实现比<实现要简单
		++min;//让日期小的那个一天一天加,直到等于大的那个日期
		++n;
	}
	return n * flag;
}


//void Date::operator<<(ostream& out) { //该函数参数顺序(Date* this, ostream& out)
//	out << _year << "/" << _month << "/" << _day << endl;
//}
cout << d1;
该函数依然无法调用,因为是双操作数,左操作数必须是第一个参数(即cout)
但该函数第一个操作数是调用该函数的对象d1

//void operator<<(ostream& out, const Date& d) {
//	out << d._year << "/" << d._month << "/" << d._day << endl;
//}

//支持连续流插入
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;
}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"

void TestData1() {
	Date d1(2024, 8, 20);
	d1.Print();

	Date d2;
	d2.Print();

	Date d3(2023, 13, 25);
	d3.Print();
}

void TestData2() {
	Date d1(2024, 8, 20);
	//拷贝构造,一个已经存在对象去初始化另一个要创建对象
	Date d2(d1);

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

	Date d3(2024, 7, 8);
	//赋值,两个已经存在对象进行拷贝
	d1 = d3;//编译器转换为d1.operator=(d3)
	d1.Print();
}

void TestData3() {
	Date d1(2024, 8, 21);
	d1 += 180;
	d1.Print();

	Date d2(2024, 8, 21);
	Date ret = d2 + 180;
	ret.Print();

	//Date d3(2024, 8, 21);
	拷贝构造
	//Date ret = d3;
	//Date ret(d3 + 180);
	//Date ret = d3 + 180;
	赋值
	//Date ret;
	//ret = d3 + 180;

}

void TestData4() {
	Date d1(2024, 8, 21);
	d1 -= 2000;
	d1.Print();

	Date d2(2024, 8, 26);
	d2 += -200;
	d2.Print();

	Date d3(2024, 8, 26);
	d3 -= -200;
	d3.Print();
}

void TestData5() {
	Date d1(2023, 7, 27);
	
	Date ret1 = d1++;
	//Date ret1 = d1.operator++(0);//可以显示调用
	ret1.Print();
	d1.Print();

	Date ret2 = ++d1;
	//Date ret2 = d1.operator++();
	ret2.Print();
	d1.Print();
}

void TestData6() {
	Date d1(2024, 8, 26);
	Date d2(1991, 12, 5);

	cout << (d1 - d2) << endl;
}

void TestData7() {
	//void Print();
	const Date d1(2024, 8, 26);
	//d1.Print();//无法调用。
	//d1被const修饰,d1地址类型就是const Date*(不能修改指向的内容)。
	//d1调用Print函数是把d1的地址传过去,而Print函数的参数本质是Date* this,所以这里权限被放大了。
	//将函数改为void Print() const; 这样不论对象是否被const修饰都可以调用该函数。
	//没有被const修饰的对象是一种权限的缩小
}

void TestData8() {
	Date d1(2024, 9, 2);
	Date d2(2024, 9, 3);
	//不支持这种打印方式,因为d1是自定义类型
	//cout << d1;
	//cin >> d1;

	重载以后依然不能用cout << d1这种方式调用,因为参数顺序不匹配
	//d1 << cout;//虽然可以,但不符合使用习惯

	改为全局定义,并添加友元声明
	//cout << d1;

	//支持连续流插入版本
	cout << d1 << d2;
	cin >> d1;
	cout << d1;
}

int main() {
	//TestData1();
	//TestData2();
	//TestData3();
	//TestData4();
	//TestData5();
	//TestData6();
	//TestData7();
	TestData8();
	return 0;
}

第七章:const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

class Date {
public:
	Date(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	
	void Print() {
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
	
	//可以被const对象调用的版本
	//void Print() const { //这里的const本质是void Date::Print(const Date* this) 不能修改this指向的对象
	//	cout << _year << "年" << _month << "月" << _day << "日" << endl;
	//}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main() {
	const Date d1(2022, 1, 13);
	d1.Print();
	//d1被const修饰,d1地址类型就是const Date*(不能修改指向的内容)。
	//d1调用Print函数是把d1的地址传过去,而Print函数的参数本质是Date* this,所以这里权限被放大了。
	//将函数改为void Print() const; 这样不论对象是否被const修饰都可以调用该函数。
	//没有被const修饰的对象是一种权限的缩小
	return 0;
}

const成员函数重载

以顺序表作为例子

#include <iostream>
#include <assert.h>
using namespace std;

struct SeqList {
public:
	void PushBack(int x) {
		//扩容
		_a[_size++] = x;
	}
    
    //版本1
	int operator[](size_t i) { //返回数组i下标处的值
		assert(i < _size);
		return _a[i];//这里返回的是_a[i]的拷贝(即临时对象),具有常性。不能修改
		//_a 是类的私有成员,外部无法直接访问。通过 operator[] 提供一个接口,能在控制访问的同时保护数据的封装性。
	}
    
    //版本2
    //上方数组下标运算符重载是传值返回,只能读不能写。
    //又因为数组开辟在堆上,出了作用域还在,可以传引用返回。
    //且这样做的好处可以修改,即实现++等操作
    int& operator[](size_t i) {
    	assert(i < _size);
    	return _a[i];
    }


	size_t size() { return _size; }//不能直接访问类的私有成员,所以需要函数调用

private:
	int* _a = (int*)malloc(sizeof(int) * 10);
	size_t _size = 0;
	size_t _capacity = 0;
};

int main() {
	SeqList s;
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	for (size_t i = 0; i < s.size(); i++) 
		cout << s[i] << endl;
	
	return 0;
}

上方数组下标运算符重载函数在添加了打印数据函数后出现新的问题:打印数组是不会修改数组的数据,所以打印函数的参数一般都加const

struct SeqList {
public:
	void PushBack(int x) {
		//扩容
		_a[_size++] = x;
	}

	size_t size() { return _size; }//不能直接访问类的私有成员,所以需要函数调用
	
	int& operator[](size_t i) {
		assert(i < _size);
		return _a[i];
	}

private:
	int* _a = (int*)malloc(sizeof(int) * 10);
	size_t _size = 0;
	size_t _capacity = 0;
};

//只是打印,所以加const防止修改。且传值要拷贝开销太大,所以传引用传参
void Print(const SeqList& sl) {
	//这里的sl是const对象,不能调用非const成员函数 size_t size(); 和下标重载运算符函数
	//所以要修改上方的size函数和运算符重载函数
	for (size_t i = 0; i < sl.size(); i++)
		cout << sl[i] << " ";
	cout << endl;
}

int main() {
	SeqList s;
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	for (size_t i = 0; i < s.size(); i++)
		cout << s[i] << endl;

	Print(s);
	return 0;
}

当打印函数的参数加上const后出现新问题:打印函数的参数sl是const对象,不能调用非const成员函数 size_t size(); 和下标重载运算符函数。所以size函数和运算符重载函数都要加const

#include <iostream>
#include <assert.h>
using namespace std;

struct SeqList {
public:
	void PushBack(int x) {
		//扩容
		_a[_size++] = x;
	}

	//下方打印函数的参数是const对象,且需要调用该函数,所以该函数的参数也要加const
	size_t size() const { return _size; }

	//该运算符重载函数和上方size函数同理也需要对参数加const
	int& operator[](size_t i) const {
		assert(i < _size);
		return _a[i];
	}

private:
	int* _a = (int*)malloc(sizeof(int) * 10);
	size_t _size = 0;
	size_t _capacity = 0;
};

//只是打印,所以加const防止修改。且传值要拷贝开销太大,所以传引用传参
void Print(const SeqList& sl) {
	//这里的sl是const对象,不能调用非const成员函数 size_t size(); 和下标重载运算符函数
	//所以要修改上方的size函数和运算符重载函数
	for (size_t i = 0; i < sl.size(); i++)
		cout << sl[i] << " ";
	cout << endl;
}

int main() {
	SeqList s;
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	for (size_t i = 0; i < s.size(); i++)
		cout << s[i] << endl;

	Print(s);
	return 0;
}

上方代码仍有bug未修复,虽然打印函数里sl对象被const修饰,但仍可以修改数组的数据

#include <iostream>
#include <assert.h>
using namespace std;

struct SeqList {
public:
	void PushBack(int x) {
		//扩容
		_a[_size++] = x;
	}

	size_t size() const { return _size; }

	int& operator[](size_t i) const {
		assert(i < _size);
		return _a[i];
	}

private:
	int* _a = (int*)malloc(sizeof(int) * 10);
	size_t _size = 0;
	size_t _capacity = 0;
};

//只是打印,所以加const防止修改。且传值要拷贝开销太大,所以传引用传参
void Print(const SeqList& sl) {
	for (size_t i = 0; i < sl.size(); i++) {
		sl[i]++;//虽然sl是const对象,但这里合法,且可以正常运行
		//解答:const修饰sl指的是不能修改成员变量即(_a,_size,_capacity),_a是指针,不能修改它的指向
		//      但sl[i]是数组里面的数据,可以被修改
		cout << sl[i] << " ";
	}
	cout << endl;
}

int main() {
	SeqList s;
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	for (size_t i = 0; i < s.size(); i++)
		cout << s[i] << endl;

	Print(s);
	return 0;
}

解决方法:对[]函数 构造函数重载。这里的const对象调用有const修饰返回值的运算符重载函数,这样可以避免对象被修改。也就是说即需要一个可读可写的下标重载运算符函数(PushBack函数需要),也需要一个只能读的下标重载运算符函数(Print函数需要)

#include <iostream>
#include <assert.h>
using namespace std;

struct SeqList {
public:
	void PushBack(int x) {
		//扩容
		_a[_size++] = x;
	}

	size_t size() const { return _size; }

	//int& operator[](size_t i) const {
	//	assert(i < _size);
	//	return _a[i];
	//}
	//如果只有上方的下标运算符重载函数,则会出现之前提到在打印函数里可以完成s[i]++的bug
	//但仅仅只是用const修饰返回值的下标运算符重载函数,则无法实现s[i]++
	//所以需要对[]运算符重载函数 再构造一个重载函数
	//需要修改返回值调用这个,不想修改返回值调用下方
	int& operator[](size_t i) {
		assert(i < _size);
		return _a[i];
	}

	//基于下方Print函数的原因,需要对返回值加const修饰防止修改
	const int& operator[](size_t i) const {
		assert(i < _size);
		return _a[i];
	}

private:
	int* _a = (int*)malloc(sizeof(int) * 10);
	size_t _size = 0;
	size_t _capacity = 0;
};

//只是打印,所以加const防止修改。且传值要拷贝开销太大,所以传引用传参
void Print(const SeqList& sl) {
	for (size_t i = 0; i < sl.size(); i++) {
		//sl[i]++;//报错:不能给常量赋值
		cout << sl[i] << " ";
	}
	cout << endl;
}

int main() {
	SeqList s;
	s.PushBack(1);
	s.PushBack(2);
	s.PushBack(3);
	s.PushBack(4);

	for (size_t i = 0; i < s.size(); i++)
		cout << s[i] << endl;

	Print(s);
	return 0;
}

在 C++ 中,成员函数的 const 修饰符用于控制对象的可变性和访问权限:
【非 const 对象】
可以调用 const 和非 const 成员函数。
const 成员函数承诺不修改对象的状态,因此它们可以被非 const 对象调用,也可以确保在调用时对象的状态不会被修改。
【const 对象】
只能调用 const 成员函数。因为 const 对象不能修改其状态,
所以只能调用那些不会修改对象内部数据的成员函数。非 const 成员函数可能会改变对象的状态,这对于 const 对象是不允许的。
总结:const 成员函数保证不会修改对象的状态,适用于 const 对象;非 const 成员函数可以修改对象状态,适用于非 const 对象。

【const 成员函数】:必须保证不修改对象的状态(即不能修改成员变量)。
因此,const 成员函数中只能调用其他的 const 成员函数,以确保不违反这一保证。
如果 const 成员函数尝试调用非 const 成员函数,编译器会报错,因为非 const 成员函数可能会修改对象的状态,从而与 const 成员函数的承诺冲突。
【非 const 成员函数】:没有限制,可能会修改对象的状态,因此它可以调用 const 成员函数。
调用 const 成员函数不会引发矛盾,因为 const 成员函数本身不会修改对象的状态,不会对非 const 成员函数的修改行为产生影响。
 

第八章:取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date {
public:
	//需要构成重载,一个为非const对象准备,另一个为const对象准备
	Date* operator&() {
		return this;
	}
	const Date* operator&()const {
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容

作业

1. 若要对data类中重载的加法运算符成员函数进行声明,下列选项中正确的是( )

A.Data operator+(Data);
B.Data operator(Data);
C.operator+(Data,Data);
D.Data+(Data);

答案:A
A.正确
B.语法错误,缺少运算符+
C.成员函数参数过多
D.没有运算符重载关键字operator

2. 下列关于赋值运算符“=”重载的叙述中,正确的是( )

A.赋值运算符只能作为类的成员函数重载
B.默认的赋值运算符实现了“深层复制”功能
C.重载的赋值运算符函数有两个本类对象作为形参
D.如果己经定义了复制拷贝构造函数,就不能重载赋值运算符

答案:A
A. 赋值运算符在类中不显式实现时,编译器会生成一份默认的,此时用户在类外再将赋值运算符重载为全局的,就和编译器生成的默认赋值运算符冲突了,故赋值运算符只能重载成成员函数
B.默认的赋值运算符是按成员成员,属于浅赋值
C.参数只有一个,另一个通过this指针传递
D.两个函数的调用场景不同,相互没有影响

3. 哪个操作符不能被重载 ( )  

A.*
B.()
C.. (点)
D.[]
E.->

答案:C
A.可以,例如重载对象取值,典型有以后学到的智能指针
B.可以,例如以后学到的仿函数就是通过重载()实现的
C.不能,不能被重载的运算符只有5个, 点号. 三目运算?: 作用域访 问符:: 运算符sizeof 以及.*
D.可以,例如重载对象的指向,典型有以后学到的智能指针

4. 在重载一个运算符为成员函数时,其参数表中没有任何参数,这说明该运算符是 ( )

A.无操作数的运算符
B.二元运算符
C.前缀一元运算符
D.后缀一元运算符

答案:C
A.重载为成员函数时,其函数的参数个数与真实的函数参数个数会减少1个,减少的则 通过this指针进行传递,所以无参  则说明有一个参数,故错误
B.无参成员函数相当于有一个参数的全局函数,不能是二元运算符
C.正确
D.区分前缀后缀时,后缀运算需要加一个int参数

5. 已知表达式++a中的"++"是作为成员函数重载的运算符,则与++a等效的运算符函数调用形式为( )

A.a.operator++()
B.a.operator++(0)
C.a.operator++(int)
D.operator++(a,0)

A.正确
B.operator++()传递了整形参数,故为后置++,错误
C.调用函数传递类型,导致语法错误
D.参数过多,语法错误

6. 假设 AA 是一个类, AA* abc () const 是该类的一个成员函数的原型。若该函数返回 this 值,当用 x.abc ()调用该成员函数后, x 的值是( )

A.可能被改变
B.已经被改变
C. 受到函数调用的影响
D.不变

A.此成员函数被定义为const常方法,代表在函数内部不能修改任何当前对象的数据成员,因此x不可能改变
B.错误,不能被改变
C.x的值在函数内部不受任何影响
D.正确

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值