【C++】从C到C++、从面向过程到面向对象(类与对象)

文章目录

  • C++入门知识
  • C与C++的关系
  • 1. 类的引入:从结构体到类
  • 2. 类的声明和定义
  • 3. 类的作用域
  • 4. 类的访问限定符
  • 5. 面向对象特性之一:封装
  • 6. 类的实例化:对象
  • 7. 计算类对象的内存大小
  • 8. 成员函数中暗藏的this指针
  • 9. 类的六个默认生成的成员函数
    • 9.1 构造函数
    • 在声明时给成员变量默认值
    • 9.2 析构函数
    • 9.3 拷贝构造函数
    • 运算符重载
    • 9.4 赋值重载
    • 区分拷贝构造和赋值重载
    • 9.5 取地址重载(了解)
    • 最后总结默认成员函数机制
  • 10. const成员函数
  • 11. 再谈构造函数:初始化列表
  • 12. 构造函数:explicit关键字
  • 13. static成员
  • 14. 友元
  • 15. 再次理解this指针(涉及友元、输出<<与输入>>操作符函数重载)
  • 16. 内部类
  • 17. 匿名对象
  • 18. 编译器会优化构造与拷贝构造的重复调用

C++入门知识

  1. C++内联函数与宏的对比
  2. C++引用与指针的对比
  3. C++nullptr与NULL的对比
  4. C++auto关键字
  5. C++函数缺省参数
  6. C++函数重载

C与C++的关系

C语言是面向过程(procedure-oriented)的语言,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是面向对象(object-oriented)的语言,将一件事情拆分成不同的对象,靠对象之间的交互完成。

C与C++的关系是:C++即C Plus Plus,是C语言的扩展,文件后缀是.cpp。C程序可以在C++编译器下编译和运行,也就是说编写C++程序可以完全用C的语法去写。

1. 类的引入:从结构体到类

C语言结构体中只能定义变量,在C++中结构体内不仅可以定义变量,也可以定义函数,C++的结构体已经升级到了类(class)的概念。

struct Person {char name[20];int age;char gender;int height;int weight;void showInfo() {cout << name << " - " << age << " - " << gender << endl;}void sleep() {}void washCloth() {}void readBook() {}void work() {}void study() {}
};

C++中的struct可以这么做是因为需要兼容C,可以这么认为,类是结构体的升级,事实上C++更喜欢用class关键字表示一个类:

class Person
{// 类体由成员变量和成员函数组成
};

在C++中用struct表示类与用class表示类有访问权限的区别,在后面的“类的访问权限”部分中会讲到。

2. 类的声明和定义

类有两种定义方式:

  1. 成员声明和定义全部放在类体中,需要注意的是:成员函数放在类体中一起声明和定义,编译器会将这个函数当成内联函数处理。

  2. 声明和定义分开,仅成员的声明放在类体,且是写在头文件中;而成员函数的定义是写在另外一个.cpp文件中的,推荐使用这种方式。

3. 类的作用域

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

// Person.h
class Person {char name[20];int age;char gender;int height;int weight;void showInfo();void sleep();void washCloth();void readBook();void work();void study();
};
// Person.cpp
void Person::showInfo() {cout << name << " - " << age << " - " << gender << endl;
}

4. 类的访问限定符

访问限定符用于确定类成员的访问权限

  1. public:被public修饰的成员在类外可以被访问;
  2. protected:被protected修饰的成员在类外不能被访问,但可以在继承的子类中被访问;
  3. private:被private修饰的成员在类外不能被访问。

如果用struct声明和定义一个类,这个类中所有成员的默认访问权限为public,这是因为需要兼容C,C并没有访问限定符这个语法规则。使用class声明和定义一个类,成员的默认访问权限为private。

5. 面向对象特性之一:封装

面向对象的三大特性:封装、继承、多态。封装是通过private(私有的访问权限)来隐藏对象内部的属性和实现细节,控制哪些函数可以在类外部直接被使用,仅对外公开接口来和对象进行交互。

比如对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互。对于计算机使用者而言,不用关心内部核心部件,主板上线路是如何布局的,CPU内部是如何设计的。

class Person {
private:char name[20];int age;char gender;int height;int weight;
public:void showInfo();void sleep();void washCloth();void readBook();void work();void study();
};

6. 类的实例化:对象

类是对一个事物进行描述,是一个模型,定义出一个类并没有并没有分配实际的内存空间存储它。使用一个类,需要用这个类创建对象,这个过程称为“类的实例化”。一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类的成员变量。

比如上面的Person类例子,对这个Person类实例化:

Person zhangsan;
Person lisi;
Person wangwu;

7. 计算类对象的内存大小

类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
一个类的大小?

其实计算对象的内存大小与计算结构体的大小方法一致,也就是有内存对齐的规则,可以看这篇文章计算结构体的大小了解。也就是说计算对象的内存大小,其实就是计算类的成员变量大小,不包含成员函数。成员函数不会包括在内,是因为成员函数是n个对象共用的,所以存放在公共代码区给这个类的多个对象共用。

在这里插入图片描述

空类比较特殊,它也有大小,占用1个字节空间。给1字节的的逻辑可能是,如果给0字节那么将毫无意义,不如给1个字节,那么空类的作用大概率只是一个占位,表示可能以后会对其进行完善。

8. 成员函数中暗藏的this指针

this指针本质上是“成员函数”的形参,当对象调用成员函数时,编译器将对象地址作为实参传递给this形参,因此this指针并不存放在对象中,而是在栈中(可能也在寄存器中,取决于编译器)。除了static成员函数(后面会提到),每个成员函数的参数中都隐藏了一个this指针,可以调试查看。
在这里插入图片描述

this指针的类型:类的类型* const,如Person* const,this只能在“成员函数”的内部使用。this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要程序员手动传递。

下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0;
}

C.正常运行,表面存在空指针问题,但成员函数中并没有使用其它成员,仅仅只是打印一个字符串,不会导致空指针访问。

class A
{ 
public:void PrintA() {cout<<_a<<endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0;
}

B.运行崩溃,成员函数中使用了成员变量,实际上是this指针访问的成员,造成了空指针。

9. 类的六个默认生成的成员函数

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

初始化和清理:

  1. 构造函数:初始化对象;
  2. 析构函数:清理对象中申请的动态内存;

拷贝和赋值:
3. 拷贝构造:使用同类对象创建另一个之前不存在的对象;
4. 赋值重载:把一个对象赋值给另一个已经存在的对象(就是相当于赋值最常规的用法,只不过这里的赋值是改变一个对象的值);

取地址重载:
5. 普通对象取地址重载;
6. const对象取地址重载。

常常会把构造函数、析构函数、拷贝构造函数和赋值重载函数重新去自定义以满足需求,而两个取地址重载基本不会去自定义重载,下面会顺便介绍原因。

9.1 构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,在对象整个生命周期内只调用一次,构造函数并不是开空间创建对象,而是完成对象的初始化工作。C语言中经常会写一个Init()函数用于初始化,构造函数就相当于这个。

class Date {
private:int year;int month;int day;
public:Date() { // 无参构造}Date(int y, int m, int d) { // 带参构造year = y;month = m;day = d;}
};int main() {Date date1; // 通过无参构造函数初始化对象,不用跟括号。Date date2(2024, 3, 11);return 0;
}

构造函数特征如下:

  1. 函数名与类名相同。
  2. 无返回值(意思是不用写返回值,不是void的意思)。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. 无参的构造函数、全缺省的构造函数、编译器默认生成的构造函数都称为默认构造函数。这三个默认构造函数只能存在其中一个,因为这三个都可以不用传参,如果同时存在,调用时会产生歧义。
class Date {
private:int year;int month;int day;
public:Date() { // 与下面全缺省的构造函数存在冲突}Date(int y = 2024, int m = 2, int d = 22) {year = y;month = m;day = d;}
};

在这里插入图片描述
7.默认构造函数会对类中其它自定类型成员调用的它的默认构造函数,比如Date类中如果包含一个Time类成员:

class Time {
private:int hour;int minute;int second;
public:Time() {cout << "Time()" << endl;}
};class Date {
private:int year;int month;int day;Time time;
public:Date(int y = 2024, int m = 2, int d = 22) {cout << "Date(int, int, int)" << endl;year = y;month = m;day = d;}
};int main() {Date date;return 0;
}

在这里插入图片描述

在声明时给成员变量默认值

由于默认生成的构造函数不会进行有效的初始化,给的是随机值,所以C++11开始可以给内置类型(int/char/double等)成员在类中声明时给默认值,如果没有指定初始化则初始化成默认值。

class Date
{
private:int _year = 1970;int _month = 1;int _day = 1;Time _time;
};

9.2 析构函数

与构造函数功能相反,注意不是销毁对象本身,而是对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,就是清理那些申请了内存资源的成员,释放资源。

析构函数的特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数和无返回值类型。
  3. 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
  4. 析构函数不能重载。
  5. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 4){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("Stack malloc failed.");return;}_capacity = capacity;_size = 0;}~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
  1. 和默认构造函数一样,默认析构函数对自定义类型的成员调用它的析构函数。
class Time
{
public:Time() {cout << "Time()" << endl;}~Time(){cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:int _year = 1970;int _month = 1;int _day = 1;Time _time;
};int main()
{Date d;return 0;
}

在这里插入图片描述
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。有资源申请时(malloc、new,new后面会提到),一定要重新自定义释放资源(free、delete),否则会造成资源泄漏。

9.3 拷贝构造函数

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

拷贝构造函数特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
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;}
private:int _year;int _month;int _day;
};
  1. 其它任何一个函数,只要形参不是传引用、返回值不是返回引用,调用该函数时编译器首先都会去调用拷贝构造。所以拷贝构造的形参必须是引用,如果不是引用,编译器器会报错,因为按传值的说法来看拷贝构造会引发无穷递归。
void Test1(const Date d) {// 首先会先调用拷贝构造
}
void Test2(const Date& d) {// 不会调用拷贝构造
}
Date Test3() {Date date(2024, 1, 1);return date; // 会先调用拷贝构造再返回
}
int main() {Date date1;Test1(date1); Test2(date1); Test3();
}

调用Test1(const Date d),参数不是引用,所以会先调用拷贝构造:
在这里插入图片描述

拷贝构造的参数不是引用,导致无穷递归:
在这里插入图片描述

  1. 若未显式定义拷贝构造,编译器默认生成的拷贝构造函数按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)。如果类中有需要申请内存资源的成员(有malloc、new的成员),默认的拷贝构造无法完成拷贝,需要自己显示定义完成深拷贝。比如:
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 4){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("Stack malloc failed.");return;}_capacity = capacity;_size = 0;}Stack(const Stack& rStack) {_capacity = rStack._capacity;_size = rStack._size;_array = (DataType*)malloc(sizeof(DataType) * _capacity);memcpy(_array, rStack._array, _size);//for (int i = 0; i < _size; ++i) {//	_array[i] = rStack._array[i];//}}~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};

如果使用编译器默认生成的拷贝构造传入stack1初始化stack2,stack2的array仅仅只是把stack1的array地址值拷贝过来了,意味着共用同一块内存。
在这里插入图片描述
自定义完成深拷贝的拷贝构造,有内存资源申请的成员,地址不一样:
在这里插入图片描述
为了提高程序效率,一般对象传参时,尽量使用引用类型;函数返回值根据实际场景,能用引用尽量使用引用,因为不返回引用的函数,实际上返回前都会去调用拷贝构造,这样会耗费一些时间和临时占用一部分内存空间。

运算符重载

在学习赋值重载之前,还需要了解运算符重载,因为赋值本身也是一种运算符,篇幅较长,不过很好理解。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,如operator+、operator-等。

  1. 不是所有运算符都能重载,常见能重载的运算符有:+、+=、-、-=、++和–(有规定如何区分前置和后置)、>、>=、<、<=、!=、=。
  2. 运算符重载函数必须有一个类的类型参数。
  3. 流插入 << 操作符、流提取 >> 操作符也可以重载。

下面以实现Date日期类操作理解运算符重载:

Date类声明:

#include <iostream>
#include <cassert>
using std::cout;
using std::endl;int dayOfMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };class Date {
private:int _year;int _month;int _day;
public:// 全缺省的构造函数Date(int y = 1970, int m = 1, int d = 1) {_year = y;_month = m;_day = d;}// 拷贝构造函数、析构函数、赋值重载用编译器默认生成的//Date(const Date& d) {}//~Date() {}//Date& operator=(const Date& d) {} 这个后面会自定义实现/* 运算符重载 */bool operator>(const Date& d);bool operator>=(const Date& d);bool operator==(const Date& d);bool operator<(const Date& d);bool operator<=(const Date& d);bool operator!=(const Date& d);// 获取某年某月的天数int GetDayOfMonth(int year, int month) {assert(month >= 1 && month <= 12);// 2月并且是闰年return month == 2  && (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0))? 29 : dayOfMonth[month];}// 日期+天数Date& operator+=(int day);// 日期+天数Date operator+(int day);// 日期-天数Date operator-(int day);// 日期-=天数Date& operator-=(int day);// 日期-日期 返回天数int operator-(const Date& d);Date& operator++(); // 前置++Date operator++(int); // 后置++Date& operator--();Date operator--(int);void Show() {cout << _year << "-" + _month << "-" << _day << endl;}
};

Date类的成员函数定义(实现这些运算符重载函数):

bool Date::operator>(const Date& d) {if (_year > d._year) {return true;}if (_year == d._year) {if (_month > d._month) {return true;}if (_month == d._month) {return _day > d._day;}}return false;
}bool Date::operator>=(const Date& d) {//return *this > d || *this == d; //		 this->operator>(d) || this->operator==(d);return !(*this < d);
}bool Date::operator==(const Date& d) {return _year == d._year&& _month == d._month&& _day == d._day;
}bool Date::operator<(const Date& d) {//return !(this->operator>=(d));return !(*this >= d);
}bool Date::operator<=(const Date& d) {//return !(this->operator>(d));return !(*this > d);
}bool Date::operator!=(const Date& d) {return _year != d._year|| _month != d._month|| _day != d._day;//return !(*this == d);
}/* 先实现+=,再利用已经实现的+=实现+ */
Date& Date::operator+=(int day) {_day += day;int dayOfMonth = GetDayOfMonth(_year, _month);while (_day > dayOfMonth) {_day -= dayOfMonth;++_month;if (_month > 12) {_month = 1;++_year;}dayOfMonth = GetDayOfMonth(_year, _month);}return *this;
}Date Date::operator+(int day) {// Date newdate = *this;// 不是调用了赋值重载,实际是调用拷贝构造,// 因为*this已经存在,而date开始不存在,详见区分拷贝构造和赋值重载部分。Date newdate(*this); // 或Date newdate = *this; newdate += day; // newdate.operator+=(day);return newdate;
}/* 先实现+,再利用已经实现的+实现+=(不推荐) */
//Date& Date::operator+=(int day) {
//	*this = *this + day; // (*this).operator+(day);
//	return *this;
//}
//
//Date Date::operator+(int day) {
//	Date newdate(*this);
//	newdate._day += day;
//	int dayOfMonth = GetDayOfMonth(newdate._year, newdate._month);
//	while (newdate._day > dayOfMonth) {
//		newdate._day -= dayOfMonth;
//		++newdate._month;
//		if (newdate._month > 12) {
//			newdate._month = 1;
//			++newdate._year;
//		}
//		dayOfMonth = GetDayOfMonth(newdate._year, newdate._month);
//	}
//	return newdate;
//}Date Date::operator-(int day) {Date newdate(*this);newdate.operator-=(day); // newdate -= day;return newdate;
}Date& Date::operator-=(int day) {_day -= day;while (_day < 1) {--_month;if (_month < 1) {_month = 12;--_year;}_day += GetDayOfMonth(_year, _month);}return *this;
}int Date::operator-(const Date& d) {int amountDays1 = _day;int month = _month - 1;for (int y = _year; y > 0; --y) {for (int m = month; m > 0; --m) {amountDays1 += GetDayOfMonth(y, m);}month = 12;}int amountDays2 = d._day;month = d._month - 1;for (int y = d._year; y > 0; --y) {for (int m = month; m > 0; --m) {amountDays2 += GetDayOfMonth(y, m);}month = 12;}int gapDay = amountDays1 - amountDays2;return gapDay >= 0 ? gapDay : gapDay * -1;
}// 前置++
Date& Date::operator++() {*this += 1; // (*this).operator+=(1);return *this;
}
// 后置++
Date Date::operator++(int) {Date newdate(*this);*this += 1; return newdate;
}Date& Date::operator--() {*this -= 1; return *this;
}
Date Date::operator--(int) {Date newdate(*this);*this -= 1;return newdate;
}

重载前置++或前置–规定返回值必须是引用,其实就是提前–完返回自身。重载后置++或后置++规定返回值不能是引用(不能返回自身,因为得返回++前或–前的值),而且必须有一个int符在参数列表占位,这个参数没有实际作用,纯粹就是用来表明是重载后置++或后置–。

调用运算符重载函数可以正常像函数调用一样,也可以像正常的运算符一样使用:

Date d1(2024, 4, 12);
Date d2(2022, 4, 14);
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;cout << d1.operator>(d2) << endl;
cout << (d1 > d2) << endl;cout << d1.operator>=(d2) << endl;
cout << (d1 >= d2) << endl;cout << d1.operator<(d2) << endl;
cout << (d1 < d2) << endl;cout << d1.operator<=(d2) << endl-
cout << (d1 <= d2) << endl;cout << d1.operator!=(d2) << endl;
cout << (d1 != d2) << endl;Date d3(d2);
Date& tempRefd3 = d3.operator+=(19);
// Date& tempRefd3 = d3 += 19;Date d4 = d3.operator+(19);
// Date d4 = d3 + 19;Date& tempRefd3 = d3.operator-=(9);
//Date& tempRefd3 = d3 -= 9;Date& tempRefd3 = d3.operator-=(19);
//Date& tempRefd3 = d3 -= 19;cout << "d1 - d2:" << d1.operator-(d2) << "天" << endl;
cout << "d1 - d2:" << d1 - d2 << "天" << endl;d1.operator+=(100);
d2 += 2000;
Date date1 = d1.operator+(100);
Date date2 = d2 + 2000;d1.operator-=(100);
d2 -= 563;
Date date1 = d1.operator-(100);
Date date2 = d2 - 563;

9.4 赋值重载

编译器会默认生成一个赋值重载函数,效果和下面这个自定义实现一样。

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

重点是返回引用和*this的写法。

一般情况下不需要去自定义实现,需要自定义实现的场景可以看下面的注意事项。自定义实现需要注意的是:

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

  2. 编译器生成的默认赋值运算符重载函数,以值的方式逐字节拷贝(也就是值拷贝,常说的浅拷贝)。内置类型成员变量是直接赋值的,而自定义类型成员变量会调用对应类的赋值运算符重载完成赋值,因为自定义类型成员中究其到底也是内置类型成员。

所以由于编译器默认生成的赋值重载是浅拷贝,如果类中有成员申请内存资源,那么使用默认的赋值重载函数是不合适的,这会导致两个对象共用一块空间,这与之前的拷贝构造函数一样,需要自定义实现完成深拷贝

区分拷贝构造和赋值重载

拷贝构造和赋值重载看起来作用是一样的,但它们有不同的作用和机制。

  1. 拷贝构造创建一个对象,这个对象之前是不存在的。
  2. 赋值重载是对一个对象赋值,这个被赋值的对象其实是已经存在的对象了,所以赋值重载的作用就是就是修改一下值而已,本身赋值的作用就是这样。

那上面实现的Date类举例,可以更好地理解他们的区别。现在还有很多人喜欢这么写,看起来是赋值重载,其实不是:

Date Date::operator+(int day) {Date newdate = *this;   // Date newdate = *this;不是调用了赋值重载,实际是调用拷贝构造,// 因为*this已经存在,而newdate最开始不存在。// 所以这样写和写成拷贝构造的方式区别不大:Date newdate(*this);  newdate += day; // newdate.operator+=(day);return newdate;
}

9.5 取地址重载(了解)

这两个取地址重载函数不用自己去定义,用编译器默认生成的就好了。非要自定义实现的话,也很简单:

class Date {
private:int _year;int _month;int _day;
public:Date* operator&() {return this;}// 最后的const表示const成员,和前面那个返回值的const修饰作用不一样,后面会提到。const Date* operator&() const {return this;}
}

如果连这个&引用重载都要自己去定义,那就太麻烦了,毕竟这是使用频率非常高的操作符。C++设计者做了这个语法,我估计完全就是为了完成操作符重载这一概念的逻辑闭环,确实得有这个东西,但一般很少去自己去重新定义。

除非有啥特殊的需求,就是要去重新定义,比如返回空指针、假地址、野指针之类的,但即使是这种需求感觉意义也不大,我看就是恶搞故意整一个bug。比如:

Date* operator&() {return nullptr;
}
const Date* operator&() const {return (const Date*)0x0012ff40;// int a = 10;// return (const Date*)&a;
}

在这里插入图片描述

最后总结默认成员函数机制

构造函数和析构函数:不处理内置类型成员,对于自定义类型成员会去调用它自己的构造函数和析构函数。

拷贝构造函数和赋值重载函数:处理内置类型成员,不过仅完成值拷贝(浅拷贝)。对于自定义类型成员会去调用它自己的拷贝构造函数和赋值重载函数。

10. const成员函数

const修饰成员函数(注意修饰的写法),实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

void ShowDate() const {cout << _year << "-" << _month << "-" << _day << endl;
}

可以理解为这样(实际不允许这么写):

void Print(const Date* this) {cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

不要与const修饰返回值混淆:

const Date GetDate() const {return *this;
}

请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗?不可以。
  2. 非const对象可以调用const成员函数吗? 可以
  3. const成员函数内可以调用其它的非const成员函数吗?不可以
  4. 非const成员函数内可以调用其它的const成员函数吗? 可以

这几个关于const与非const的问题,其实就是权限的问题。正常的变量、函数允许读写擦欧洲哦,而const修饰后的变量或函数只允许读操作(只读),也就是说这个const变量不能被修改const函数内不能修改任何成员的值。

权限缩小是可以的,权限放大不行。通过这个结论用几句话总结上面四个问题:const不能调用非const,非const可以调用const。

11. 再谈构造函数:初始化列表

构造函数给对象中各个成员变量一个合适的初始值,不过构造函数中函数体的语句不能称之为初始化,只能称为赋初值。初始化只能一次,而函数体内赋初值却可以多次重复。

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

要想真正只初始化一次,C++构造函数提供初始化列表的语法:

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成员变量。
  3. 没有默认构造函数的自定义类型成员。(默认构造函数有三种:1.编译器生成的默认构造函数;2.自定义的无参构造函数;3.全缺省参数的构造函数。这三种默认构造函数的相同点都是可以不进行传参调用)。
class A
{
public:A(int a):_member(a){}
private:int _member;
};class B
{
public:B(int a, int ref):_aobj(a),_ref(ref),_n(10){}
private:A _aobj;  // 没有默认构造函数int& _ref;  // 引用const int _n; // const 
};

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

初始化列表有一个特性:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。看例子:

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();
}

A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值

上面程序的运行结果,正常选恐怕都会选A,但其实答案D,上面的特性说明的很清楚了。

12. 构造函数:explicit关键字

对于单个参数,或除第一个参数无默认值、其余均有默认值的构造函数,或全缺省参数的构造函数,它们具有类型转换的作用。

class Date
{
public:Date(int y):_year(y){}// 或// Date(int y, int m = 1, int d = 1)//	:_year(y)//	,_month(m)//	,_day(d)// {}// 或全缺省构造函数
private:int _year;int _month;int _day;
};

用一个整形变量给日期类型对象赋值,实际编译器背后会用2023构造一个匿名对象,最后用匿名对象给d1对象进行赋值,这一流程就是类型转换。

void Test() {Date d(2024, 1, 1);d = 2023; 
}

如果要禁止这一类型转换功能,用explicit关键字给构造函数修饰就行:

explicit Date(int y = 1970, int m = 1, int d = 1):_year(y),_month(m),_day(d)
{
}

在这里插入图片描述

13. static成员

用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。以下是使用static成员的重要注意事项:

  1. 静态成员变量声明的时候需要加static关键字,但不能声明的同时初始化,一定要在类外进行初始化,并且使用作用域操作符::表明该static成员变量属于哪个类。
class A {
private:static int _count;}int A::_count = 0;
  1. 静态成员为所有类对象所共享,不属于某个具体的对象,并且存放在静态区。因此可以用类名::静态成员、或对象.静态成员两种方式访问。
cout << A::_count << endl;
A a;
cout << a._count << endl;
  1. 静态成员函数没有隐藏的this指针,因此不能访问任何非静态成员。
  2. 静态成员受public、protected、private访问限定符的影响。

面试题:实现一个类,计算程序中创建出了多少个类对象。(静态成员变量解决)

常见问题:

  1. 静态成员函数可以调用非静态成员函数吗?不可以,没有this。
  2. 非静态成员函数可以调用类的静态成员函数吗?可以,有this。

14. 友元

15. 再次理解this指针(涉及友元、输出<<与输入>>操作符函数重载)

16. 内部类

17. 匿名对象

18. 编译器会优化构造与拷贝构造的重复调用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/289263.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

哪个洗地机性价比比较高?这几款品牌值得入手

洗地机是现在越来越受欢迎的清洁工具&#xff0c;功能非常齐全。它就像是吸尘器和电动拖把的结合体&#xff0c;对于每天要做家务的人来说&#xff0c;可以一次性解决多种类型的垃圾&#xff0c;省时省心省力。可是面对种类繁杂的洗地机市场&#xff0c;我们该如何选择呢&#…

如何在群晖NAS搭建bitwarden密码管理软件并实现无公网IP远程访问

前言 作者简介&#xff1a; 懒大王敲代码&#xff0c;计算机专业应届生 今天给大家聊聊如何在群晖NAS搭建bitwarden密码管理软件并实现无公网IP远程访问&#xff0c;希望大家能觉得实用&#xff01; 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&am…

python_1

要求&#xff1a; 代码&#xff1a; # 先将分钟数转化成年数&#xff0c;再将余数做为天数 minute float(input("请输入分钟数&#xff1a;")) year_1 (minute / 60 / 24) // 365 day_1 (minute / 60 / 24) % 365 now f"{minute}分钟{year_1}年{day_1}天&q…

值得收藏!2024年智能交通领域顶级会议投稿信息汇总

智能交通系统&#xff08;Intelligent Transportation Systems, ITS&#xff09;是应用现代信息技术、通信技术、数据处理技术、控制技术和传感技术于交通管理系统中&#xff0c;旨在提高道路安全性、提升交通效率、改善交通环境、增加驾驶舒适度和效率的综合智能化系统。它通过…

【C++初阶】之类和对象(中)

【C初阶】之类和对象&#xff08;中&#xff09; ✍ 类的六个默认成员函数✍ 构造函数&#x1f3c4; 为什么需要构造函数&#x1f3c4; 默认构造函数&#x1f3c4; 为什么编译器能自动调用默认构造函数&#x1f3c4; 自己写的构造函数&#x1f3c4; 构造函数的特性 ✍ 拷贝构造…

淘宝商品评论抓取技术大揭秘:轻松获取用户评价,助力电商决策!

获取淘宝商品评论接口的技术实现涉及多个步骤&#xff0c;包括获取商品ID、构建请求URL、发送HTTP请求以及解析响应数据。以下是一个基本的指南和示例代码&#xff0c;帮助你了解如何实现这一功能。 步骤一&#xff1a;获取商品ID 首先&#xff0c;你需要知道你想要获取评论的…

fuzzywuzzy,一个好用的 Python 库!

目录 前言 安装 基本功能 1. 字符串相似度比较 2. 模糊匹配与排序 实际应用场景 1. 数据清洗 2. 文本匹配与搜索 3. 搜索引擎优化 总结 前言 大家好&#xff0c;今天为大家分享一个好用的 Python 库 - fuzzywuzzy Github地址&#xff1a;https://github.com/seatgeek/fu…

Stable Diffusion WebUI 图生图(img2img):图生图/涂鸦绘制/局部重绘/有色蒙版/上传蒙版/批量处理/反推提示词

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 大家好&#xff0c;我是水滴~~ 本篇文章我们介绍 Stable Diffusion WebUI 的图生图功能&#xff0c;主要包括&#xff1a;图生图、图生图&#xff08…

Spring-IoC-属性注入的注解实现

1、创建对象的注解 Component 用于声明Bean对象的注解&#xff0c;在类上添加该注解后&#xff0c;表示将该类创建对象的权限交给Spring容器。可以直接将这些类直接创建&#xff0c;使用的时候可以直接用。 注解的value属性用于指定bean的id值&#xff0c;value可以省略&…

BioXcell InVivoPlus anti-mouse Ly6G及部分参考文献

BioXcell InVivoPlus anti-mouse Ly6G 1A8单克隆抗体与小鼠Ly6G反应。Ly6G分子量为21-25kDa&#xff0c;是GPI锚定的细胞表面蛋白Ly-6超家族的成员&#xff0c;在细胞信号传导和细胞粘附中发挥作用。Ly6G在发育过程中由骨髓谱系中的细胞&#xff08;包括单核细胞、巨噬细胞、粒…

在点集的新知识面前百年集论不堪一击

黄小宁 与x∈R相异&#xff08;等&#xff09;的实数均可表为yxδ&#xff08;增量δ可0也可≠0&#xff09;&#xff0c;因各实数的绝对值都可是表示长度的数故各实数都可是数轴上点的坐标&#xff0c;于是x∈R变换为实数yxδ的几何意义可是&#xff1a;一维空间“管道”g内R…

产品推荐 | 基于 Xilinx ZYNQ UltraScale+的FACE-ZUSSD-C 多核SOC设计开发平台

01、产品概述 FACE-ZUSSD-C多核SOC设计开发平台是FACE系列的新产品。FACE-ZUSSD-C搭载有16nm工艺的ZYNQ UltraScale系列主器件ZU19EG。该主器件具有丰富的FPGA可编程逻辑资源&#xff0c;同时其内嵌有四核ARM CortexA53&#xff0c;双核ARM CortexR5以及Mali400 GPU。 平台板…

常用的苹果应用商店上架工具推荐

摘要 移动应用app上架是开发者关注的重要环节&#xff0c;但常常会面临审核不通过等问题。为帮助开发者顺利完成上架工作&#xff0c;各种辅助工具应运而生。本文探讨移动应用app上架原理、常见辅助工具功能及其作用&#xff0c;最终指出合理使用工具的重要性。 引言 移动应…

构建一个基础的大型语言模型(LLM)应用程序

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

通用指南-营销和设计中的增强现实(AR)

原文作者&#xff1a;Superside 翻译&#xff1a;数字化营销工兵 --- 经典万字长文&#xff0c;权威解读&#xff0c;分享经典&#xff0c;预计阅读完需要30分钟&#xff0c;建议收藏&#xff01; 目录 一、引言 为什么要尝试AR AR到底是什么&#xff1f;营销人员和创意人…

镭速如何适配国产数据库(达梦)进行高效数据管理与共享

在当今企业运营的背景下&#xff0c;数据的管理和共享显得尤为关键。在这个过程中&#xff0c;挑选一个合适的数据库系统是至关重要的。国产的达梦数据库因其在多个关键领域&#xff0c;如金融、能源、航空和通信等的成功运用&#xff0c;已经成为众多企业的首选解决方案。 作为…

【Git篇】复习git

文章目录 &#x1f354;什么是git⭐git和svn的区别 &#x1f354;搭建本地仓库&#x1f354;克隆远程仓库&#x1f6f8;git常用命令 &#x1f354;什么是git Git是一种分布式版本控制系统&#xff0c;它可以追踪文件的变化、协调多人在同一个项目上的工作、恢复文件的旧版本等…

后端程序员入门react笔记(九)- react 插件使用

setState setState引起的react的状态是异步的。操作完毕setState之后如果直接取值&#xff0c;可能取不到最新的值&#xff0c;我们举个例子console.log(this.state.num)打印的值&#xff0c;总是上一次的值而不是最新的。 import React, {Component} from react; class Ap…

SQLiteC/C++接口详细介绍sqlite3_stmt类(十三)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍sqlite3_stmt类&#xff08;十二&#xff09; 下一篇&#xff1a; SQLite数据库文件损坏的可能几种情况 51、sqlite3_stmt_scanstatus_reset sqlite3_stmt_scanstatus_reset 函数用于重置指…

机器学习:探索数据中的模式与智能

文章目录 导言介绍&#xff1a;机器学习的定义和重要性发展历程&#xff1a;从概念到现实应用 基础概念机器学习的基本原理监督学习、无监督学习和强化学习的区别与应用1.监督学习2.无监督学习3.强化学习 常见的机器学习任务和应用领域 结语 导言 当代科技领域中最为引人注目的…