C++类和对象

目录

1.面向过程和面向对象初步认识

2.类的引入

3.类的定义

4.类的访问限定符及封装

5.类的作用域

6.类的实例化

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

8.类成员函数的this指针

9.类的6个默认成员函数

10.构造函数

11.析构函数

12.拷贝构造函数

13.赋值运算符重载

14.const成员函数

15.取地址及const取地址操作符重载

16.再谈构造函数

17.Static成员

18.友元

19.内部类

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


1.面向过程和面向对象初步认识

        C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。以洗衣服为例,C语言是这样做的:拿盆——放水——放洗衣粉——放衣服——手搓——换水——清洗——拧干——晾衣服。

C++把这件事分为四个对象:人、衣服、洗衣粉、洗衣机
整个洗衣服的过程:人将衣服放进洗衣机、倒入洗衣粉,启动洗衣机,洗衣机就会完成洗衣过程并且甩干
整个过程主要是:人、衣服、洗衣粉、洗衣机四个对象之间交互完成的,人不需要关新洗衣机具体是如何洗衣服的,是如何甩干的。
 

2.类的引入
 

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数,用class关键字代替struct

        


	class person //class 类的关键字
	{
	public:
		person(int age, string name) 
		{
			_age = age;
			_name=name;
		}
		void print()   //类的内部可以定义函数
		{
			cout << "姓名:" << _name << " " << "年龄:" << _age << endl;
		}
	private:
		int _age;
		string _name;
	};

int main()
{
	person s( 19,"张三");
	s.print();
	return 0;
}

3.类的定义

class classname
{
	//类体:由成员函数和成员变量
};//注意分号

class为定义类的关键字,ClassName为类的名字{}中为类的主体,注意类定义结束时后面分号不能省略。 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者 成员函数。 类的两种定义方式: 1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内 联函数处理。

4.类的访问限定符及封装

面向对象的三大特性:封装、继承、多态。

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢? 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。 封装本质上是一种管理,让用户更方便使用类

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

是指在类中使用 public、private 和 protected 关键字限制类成员变量和成员函数的访问权限,从而实现对类成员的封装和保护。
public(公有):访问限定符表示在类的外部和类的派生类中都可以访问该成员,相当于在类的接口中公开了这个成员,是最为开放的访问级别。
private(私有):访问限定符表示只能在类内部访问该成员,即使是同一命名空间或其他技术手段也无法访问,类似于该成员是类的一个秘密,是最为保守的访问级别。
protected(保护):访问限定符表示只能在类内部和该类的派生类中访问该成员,派生类可以通过继承函数访问到它,是类成员的半开放性访问。

将成员变量设置为 private,将成员函数设置为 public 或 protected,可以有效防止外部程序对类的实现细节进行干扰和修改,同时提高了代码的安全性和可维护性。
【访问限定符说明】

1. public修饰的成员在类外可以直接被访问。

2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。

3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

4. 如果后面没有访问限定符,作用域就到 } 即类结束。

5. class的默认访问权限为private,struct为public(因为struct要兼容C) 注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

5.类的作用域

C++类的作用域是指可以访问该类定义的成员所在的范围。一般来说,类定义的作用域包含了以下内容:
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
1. 类的成员函数的定义和声明;
2. 类的数据成员的定义和声明;
3. 类的嵌套类型的定义和声明;
4. 类的静态成员的定义和声明。

类的作用域可以通过以下两种方式来访问:

1. 类作用域限定符:使用关键字"::"来限定类作用域。例如,类名::成员名表示访问该类中的成员。

2. 类实例成员限定符:使用类的实例来访问该类的成员。例如,实例名.成员名表示访问该实例中的成员。

class person
	{
	public:
		person(int age, string name) 
		{
			_age = age;
			_name=name;
		}

	void show();
		
	private:
		int _age;
		string _name;
	};

void person::show()//指定show是person类域的
{
	cout << "姓名:" << _name << " " << "年龄:" << _age << endl;
}

int main()
{
	person s( 19,"张三");
	s.show();
	return 0;
}

6.类的实例化

类的实例化常常包含以下几个方面的内容:

1. 分配内存空间:类的实例需要在内存中分配空间,可以通过new运算符或者定义在栈中来分配空间。


person* person = new person(); //使用new运算符在堆中分配Person类实例
person person; //定义在栈中分配Person类实例


2. 构造函数:在分配内存空间之后,需要调用该类的构造函数进行初始化。构造函数可以没有参数,也可以有一个或多个参数。

关于构造函数之后会说。


person(){}     //默认构造函数
person(string name, int age){this->name = name; this->age = age;} //带参数的构造函数 



3. 成员函数和数据成员:类的实例包含成员函数和数据成员。可以通过点“.”运算符或者箭头“->”运算符来访问类的成员函数和数据成员。


person.name("张三"); //调用setName函数设置person的名字为张三
string name = person._name(); //调用getName函数获取person的名字
person._age = 19; //直接访问person的数据成员age并设置其值为19
int age = person.—_age; //直接访问person的数据成员_age并获取其值

这里我们先记住实例化出的对象的过程是占用实际的物理空间用来存储类成员变量的过程。就好比根据设计好的图纸,来建造相应的建筑物,类就是图纸,而建筑物就是对象。

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

7.1类中有成员变量和成员函数的大小

// 类中既有成员变量,又有成员函数
class class1 {
public:
	void f1() {}
private:
	int _a;
};


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

// 类中仅有成员变量
class class3 {
private:
	int _a;
};
// 类中什么都没有---空类
class class4
{};
int main()
{
	cout << sizeof(class1) << endl;
	cout << sizeof(class2) << endl;
	cout << sizeof(class3) << endl;
	cout << sizeof(class4) << endl;

	return 0;
}

    cout << sizeof(class1) << endl;//大小为4
    cout << sizeof(class2) << endl;//大小为1
    cout << sizeof(class3) << endl;//大小为4
    cout << sizeof(class4) << endl;//大小为1

从以上结果可以得出结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐 注意空类的大小,空类比较特殊,编译器给了空类1个字节来唯一标识这个类的对象。注意空struct(结构体)的大小为0个字节,空类大小为1个字节。

关于结构体内存对齐规则之前的文章有写过。

8.类成员函数的this指针

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
        this指针是一个内置指针,指向当前对象。当你调用某个成员函数时,this指针就会自动被赋值为调用该函数的对象的地址。你可以使用this指针来引用当前对象的成员变量和成员函数,也可以用它来返回当前对象的引用。

下面是一个示例,演示如何使用this指针:

在这个示例中,我们定义了一个名为CLass的类,并在其中定义了两个函数:setX和getX。setX函数使用this指针来设置当前对象的x成员变量,getX函数则使用this指针来获取当前对象的x成员变量的值。在主函数中,我们创建了一个CLass对象,并使用它来调用setX和getX函数。

class CLass{
public:
	void setX(int x)//可以看成 void setX(CLass* this,int x)
	{
		this->x= x;
	}

	int getX()//int getX(CLass* this)
 {
		return this->x;
	}

private:
	int x;
};

int main()
{
	CLass obj;
	obj.setX(10);


	//obj这个对象调用成员函数时,
	//this指针就会自动被赋值为调用该函数的对象(obj)的地址。
	cout<< "Value of x is: " << obj.getX() << endl;
	return 0;
}


8.1this指针的特性

1. this指针是每一个非静态成员函数的隐含形参,指向调用该成员函数时所操作对象的地址。
2. this指针只能用在类的成员函数(包括虚函数)中,在其他函数(包括全局函数和static函数)中无法使用。
3. this指针在成员函数被调用时才会被赋值,因此不能在类的声明中使用。
4. this指针是一个常量指针,不能被赋值,但可以修改指针指向的对象的值。如果要修改this指针所指向的内容,应该使用*this。
5. this指针的类型与类的类型一致,所以它可以被强制转换成类的类型指针。
6. 当对象调用成员函数时,编译器会自动将对象的地址作为实参传给该函数的this指针,一般情况由编译器通过ecx寄存器自动传递,不需要显式的传递。

9.类的6个默认成员函数

        在之前我们知道只有成员函数的类和空类的空间大小都是1个字节,这是因为对象只保存成员变量,而成员函数存放在公共的代码段。

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

6个默认成员函数 

构造函数:主要完成初始化工作初始化和清理
析构函数:主要完成清理工作
拷贝构造:是使用同类对象初始化创建对象
赋值重载:主要是把一个对象赋值给另一个对象
取地址重载 :主要是普通对象和const对象取地址,这两个很少会自己实现

10.构造函数

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(2023,4,13);
	d1.print();

	date d2;
	d2.Init(2023,3,12);
	d2.print();

	return 0;
}

        对于date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置 信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

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

10.2构造函数的特性

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

其特征如下:

1.函数名与类名相同。

2.无返回值。

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

4.构造函数可以重载。

class date
{
public:
	//无参构造函数
	date()
	{
	}

	//带参构造函数
	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 d1;//调用无参构造函数
	//error date d();
	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	d1.print();

	date d2(2023,2,2);//调用带参的构造函数
	d2.print();


	return 0;
}

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

class date
{
public:
	

	
	void print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
	
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	date d1;
	d1.print();
	return 0;
}

6.当执行以上代码,看到输出结果后,心中不免会有疑问为什么d1对象的成员变量会是随机值?好像编译器生成的默认构造函数并没有什么用唉?

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,C++11 中内置类型成员变量在 类中声明时可以给默认值。

	
private:
	int _year=1990;
	int _month=1;
	int _day=1;

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

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

int main()
{
	Date d1;//调用出现歧义
	return 0;
}

11.析构函数

        析构函数是在一个对象被销毁自动调用,它的主要作用释放对象所持有的资源,如动态分配的内存,打开的文件等等。

其特征如下:

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数

5. 关于编译器自动生成的析构函数,它能干什么?我们来看看下面的代码。

class Date
{
public:
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	
};class person
{
public:
	person()
	{
	}
private:
	string name=" ";
	int age=18;

	// 自定义类型
	Date _d;
};
int main()
{
	person p1;
	return 0;
}

        程序运行结束后输出:~Date()在main方法中根本没有直接创建Date类的对象,为什么最后会调用Date类的析构函数?
 因为:main方法中创建了person对象p1,而d中包含4个成员变量,其中name,age两个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而d是Date类对象,所以在p1销毁时,要将其内部包含的Date类的d对象销毁,所以要调用Date类的析构函数。
但是:main函数中不能直接调用Date类的析构函数,实际要释放的是person类对象,所以编译器会调用person类的析构函数,而person没有显式提供,则编译器会给person类生成一个默认的析构函数,目的是在其内部调用Date类的析构函数,即当person对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Date类析构函数,而是显式调用编译器为person类生成的默认析
构函数
 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。

到这里有很多读者,心里不禁编译器真好,那我们是不是以后都可以不用写析构函数了。

当然不是了,编译器也有它清理不了的内存。

那在什么情况下我们需要自己写析构函数呢?

如果一个类中包含了资源的分配(如在堆区上申请内存)和管理,那么就需要自己写析构函数,确保这些资源在对象被销毁时得到正确的释放。

以下是需要自己写析构函数的具体情况:

1. 类中含有指针成员变量,且这些指针指向在堆上分配的内存。此时需要在析构函数中释放这些内存,否则会造成内存泄漏。

2. 类对象包含了一种系统资源,如句柄、打开的文件、数据库连接等,需要在析构函数中进行显式释放,否则可能会造成系统资源泄漏。

3. 类中使用了定位 new 运算符,用于在特定区域分配内存,需要在析构函数中用定位 delete 运算符来释放这些内存。

4. 如果需要确保在程序结束之前关闭某些资源,比如在类中打开了一个网络连接或者管道连接等,需要在析构函数中显式关闭这些资源。

12.拷贝构造函数

        简单的说就是用一个已经存在的类对象创建一个新的对象,新对象与已经存在的对象具有相同的值及属性,即进行对象的浅拷贝,拷贝

拷贝构造函数的特征:

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

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

class Date
{
public:
	Date(int year = 2023, int month = 4, int day = 13)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	//Date(const Date d)// 错误写法:编译报错,会引发无穷递归
Date(const Date & d)   
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

3.如果没有定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数是按字节序完成拷贝,即浅拷贝。

class Date
{
public:
	Date(int year = 2023, int month = 4, int day = 13)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;

	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2002,2,2);
	//用d1创建d2  //没有显示定义拷贝构造函数  编译器会默认生成
	Date d2(d1);
	d2.print();
	return 0;
}

4.既然编译器会自动生成默认的拷贝构造函数,那什么时候我们需要自己写拷贝构造函数呢?

        一般情况下,在类中定义了指针成员变量、动态分配内存等需要进行深拷贝的成员变量时,就需要自己手动编写拷贝构造函数。
深拷贝是指拷贝一个对象的同时,也要拷贝该对象所使用的动态内存资源,这样可以避免多个对象之间共享同一块内存引发不必要的问题。由于默认的拷贝构造函数只是进行浅拷贝,即简单地复制一份源对象的数据成员,如果源对象和目标对象之间存在指向同一动态内存资源的指针,则可能导致对象析构时释放同一块内存资源,从而引发不可预期的程序错误。而手动编写拷贝构造函数可以确保对象间的数据成员不会相互干扰,避免产生不必要的后果。因此,当自定义类中存在指针成员变量、动态分配内存等需要进行深拷贝的成员变量时,我们需要自己编写拷贝构造函数,以确保程序的正常运行。


深拷贝示例程序:

class person 
{
public:

	//拷贝构造函数
	person(const person& p)
	{
		this->name  = new char[strlen(p.name) + 1];
		strcpy(this->name, p.name);
		this->age = p.age;
	}

	//拷贝构造函数
	person& operator=(const person& p)
	{
		if (this != &p)
		{
			if (this->name)
				delete[] this->name;
			this->name = new char[strlen(p.name) + 1];
			strcpy(this->name, p.name);
			this->age = p.age;
		}
		return *this;
	}

	//构造函数
	person(const char*name,int age)
	{
		this->name = new char[strlen(name) + 1];
		strcpy(this->name, name);
		this->age = age;
	}

	//析构函数
	~person()
	{
		//判断是否为空类
		if (this->name)
			delete[] this->name;
	}
	void print()
	{
		cout << "姓名:" << name <<" " << "年龄:" << age << endl;
	}

private:
	char* name;
	int age;
};
int main()
{
	person p1("张三", 30);
	person p2 = p1;
	p1.print();
	p2.print();
	person p3("李四", 22);
	p3.print();
	p3 = p1;
	p3.print();

	return 0;
}

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

A.通过值传递方式传递对象:当一个对象作为实参被传递给一个函数时,拷贝构造函数将被调用,使用原对象创建一个新对象,从而防止在函数中修改原对象。

B.在函数中返回一个对象:当函数返回一个对象时,拷贝构造函数将被调用,创建一个新对象并返回,从而返回一个和原对象值相同的新对象。

C.用一个对象初始化另一个对象:当一个对象需要被另一个初始化时,拷贝构造函数将被调用,创建一个新对象并将其初始化为指定对象的副本。

13.赋值运算符重载

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

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

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

注意:

不能通过连接其他符号来创建新的操作符:

比如operator@ 重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变。

作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this

.*    ::    sizeof     ?:     . 注意这5个运算符不能重载。

13.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 ;
};

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

class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
 if (&left != &right)
 {
 left._year = right._year;
 left._month = right._month;
 left._day = right._day;
 }
 return left;
}
// error  “operator =”必须是非静态成员

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

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

 class Time
{
public:
 Time()
 {
 _hour = 1;

_minute = 1;
 _second = 1;
 }
 Time& operator=(const Time& t)
 {
 if (this != &t)
 {
 _hour = t._hour;
 _minute = t._minute;
 _second = t._second;
 }

return *this;
 }
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;
 Date d2;
 d1 = d2;
 return 0;
}

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

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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;
 s2 = s1;
 return 0;
}

C++类和对象_第1张图片

 

前置++和后置++重载


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

关于运算符重载,后续会写一篇系统总结。

14.const成员函数

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

请思考下面的几个问题:

1. const对象可以调用非const成员函数吗?

2. 非const对象可以调用const成员函数吗?

3. const成员函数内可以调用其它的非const成员函数吗?

4. 非const成员函数内可以调用其它的const成员函数吗?

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
	void Print() const
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
void Test()
{
	Date d1(2022, 1, 13);
	d1.Print();
	const Date d2(2022, 1, 13);
	d2.Print();
}

需要注意以下几点:
1. const成员函数可以被const对象调用,而非const成员函数不能被const对象调用。
2. const成员函数内部不能修改对象的数据成员,否则将会产生编译错误。
3. const成员函数内部不能调用非const成员函数,否则将会产生编译错误。
4. const成员函数可以重载非const成员函数,但是不能重载其他const成员函数,因为它们参数列表完全一致。
5. 如果一个成员函数需要访问对象的数据成员,但不会修改它们,应该将这个函数声明为const成员函数。这有助于提高代码的可读性和将来维护的方便性。

15.取地址及const取地址操作符重载

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

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
//这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
//要重载,比如想让别人获取到指定的内容!
 }
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

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

16.再谈构造函数

1.构造函数体赋值

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

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

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

2 初始化列表

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


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. 类中包含以下成员,必须放在初始化列表位置进行初始化:
a. const 成员变量:因为 const 成员变量在声明时被初始化,之后就无法改变其值,所以必须在初始化列表中进行初始化。
b. 引用类型的成员变量:引用类型的成员变量必须在创建对象时就赋值,因此必须在初始化列表中初始化。
c. 没有默认构造函数的类类型成员变量:如果一个类类型的成员变量没有默认构造函数,则必须在初始化列表中使用参数化构造函数进行初始化。

以上这些数据成员必须在初始化列表中进行初始化,否则程序将无法编译通过。

class A
{
public:
 A(int a)
 :_a(a)
 {}
private:
 int _a;
};
class B
{
public:
 B(int a, int ref)
 :_aobj(a)
 ,_ref(ref)
 ,_n(10)
 {}
private:
 A _aobj;  // 没有默认构造函数
 int& _ref;  // 引用
 const int _n; // const 
};

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

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


class Date
{
public:
	Date(int day = 0) :_day(day)
	{
		cout << _day << endl;
	}
private:
	int _day;
	Time _t;
};


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

}


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

class Class
{
public:
    Class(int a)
        :_a1(a)
        , _a2(_a1)
    {}

    void Print() {
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2;
    int _a1;
};
int main() {
    Class aa(1);
    aa.Print();
}

17.Static成员

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

特性

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

2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

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

4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

我们来看看这两个问题

1. 静态成员函数可以调用非静态成员函数吗?

2. 非静态成员函数可以调用类的静态成员函数吗?

第一个问题:

        静态成员函数不能直接调用非静态成员函数,因为静态成员函数没有 this 指针,而非静态成员函数是与对象相关的,需要有 this 指针才能访问对象的非静态成员变量或非静态成员函数。如果静态成员函数需要调用非静态成员函数,可以通过传递一个对象的指针或引用作为参数来访问。 在静态成员函数中,可以使用类的名称来访问非静态成员函数,但需要传递对象的引用或指针作为参数,例如:


class Foo
{
public:
    void bar() 
    {

    }

    static void baz(Foo& foo)
    {

        foo.bar();
    }
};

在上面的例子中,静态成员函数 baz可以调用非静态成员函数 bar,因为它接收一个 Foo 类型的引用,可以访问该对象的成员函数。
 

第二个问题:

      非静态成员函数可以调用类的静态成员函数,因为静态成员函数可以在不创建类的对象的情况下直接调用。在非静态成员函数中可以使用类名加作用域解析运算符来访问静态成员函数,例如:


class Foo {
public:
    static void bar() 
    {
        
    }

    void baz() {
       
        Foo::bar();
    }
};


        在上面的例子中,类 Foo 有一个静态成员函数 bar 和一个非静态成员函数 baz。 baz 函数可以直接使用 Foo::bar()来调用静态成员函数 bar。这是因为在非静态成员函数中,this指针指向当前对象,而静态成员函数不需要 this 指针。因此,可以通过类名来访问静态成员函数。

18.友元

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

友元分为:友元函数和友元类

18.1 友元函数

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

C++类和对象_第2张图片

class test
{
    friend ostream& operator<<(ostream& _cout, const test& d);
    
public:
    test(int age=0, string name=" ") :_age(age), _name(name)
    {

    }

    friend  void  print();

private:
    int _age;
    string _name;

};
ostream& operator<<(ostream& _cout, const test& d)
{
    _cout << d._name << "-" << d._age << endl;
    return _cout;
}

int main() 
{
    
    test T1(19,"张三");
    cout << T1 << endl;

    return 0;
    
}

说明:

友元函数可访问类的私有和保护成员,但不是类的成员函数

友元函数不能用const修饰

友元函数可以在类定义的任何地方声明,不受类访问限定符限制

一个函数可以是多个类的友元函数 友元函数的调用与普通函数的调用原理相同

18.2 友元类

        友元类是指在一个类中定义的另一个类,这个定义的类可以访问声明为友元类的类的私有成员。友元类可以访问声明为它友元的类的私有成员,从而可以在一个类的成员函数访问另一个类的私有成员,增加了类之间的灵活性和方便性。

友元类的特性如下:

1. A是B的友元类,而C是A的子类,C也不能访问B的私有成员。

2. 友元类的关系具有单向性。如果A是B的友元类,则B不一定是A的友元类。

3. 友元类关系不具有传递性。如果A是B的友元类,B是C的友元类,那么A不一定是C的友元类。

4. 友元类关系中的函数和变量访问权限与其所在类相同,而不是其所属类。

class Time
{
    friend class Date;   // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
   // 中的私有成员变量
public:
    Time(int hour = 0, int minute = 0, int second = 0)
        : _hour(hour)
        , _minute(minute)
        , _second(second)
    {}

private:
    int _hour;
    int _minute;
    int _second;
};
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

    void SetTimeOfDate(int hour, int minute, int second)
    {
        // 直接访问时间类私有的成员变量
        t._hour = hour;
        t._minute = minute;
        t._second = second;
    }

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

19.内部类

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

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

特性:

1. 内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

3. sizeof(外部类)=外部类,和内部类没有任何关系。

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

int main()
{
    A::B b;
    b.foo(A());

    return 0;
}


 

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

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	private:
		int _a;
	};
	void f1(A aa)
	{}
	A f2()
	{
		A aa;
		return aa;
	}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	

	// 传值返回
	f2();
	cout << endl;

	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);

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

	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;

	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

你可能感兴趣的:(c++,开发语言)