C++类和对象

目录

类的定义

类的定义

限定符说明

补充

类的实例化

类对象的大小

类中隐藏的this指针

this指针的引入

this指针的特性

类的默认成员函数

构造函数

特征

缺省值

默认构造函数

析构函数

特征

拷贝构造函数

特征

运算符重载

要求

赋值运算符重载

日期中的运算符重载

构造函数

日期的比较

日期+天数:+=

日期+天数:+

日期-天数:-=

日期前置++和后置++

日期-日期

内存管理

new和delete

与malloc和free的区别

定位new

补充

cout和cin

Date的cout和cin

初始化列表

静态成员变量

静态成员函数

匿名对象

模板,泛型编程

模板参数匹配原则


类的定义

C语言是面向过程的,关注的是过程,而C++则是面向对象的。面向过程就需要关注整个流程的实现的步骤和方法,面向对象则更关注实现某个事物参与的对象有哪些。

有时候不希望对外提供实现的方法及对象的属性,只希望向用户提供对应功能的接口,用户直接使用,C语言是无法做到对数据的封装的,所以C++引入类这一概念来实现。

C语言的结构体只能定义变量,所以C语言的数据和数据的实现方法是分离的,这会导致我们既要传数据,又要传调用的结构体,很麻烦。C++的结构体既能定义变量,还能定义函数,这样就可以将数据及数据实现的方法进行有机结合。


类的定义

cpp 复制代码
class name
{        
        //内体由变量及函数组成
};   //分号

class是定义类的关键字,name是类的名称。类的成员被称为内的属性和成员变量,类的函数称为类的实现方法或成员函数。

在C++中为了实现对数据进行封装,引入了访问访问限定符的概念。

++限定符分为三种:1)public(公有);2)private(私有);3)protected(保护);++

pubic修饰的类外可以直接访问,private和protected类外是不能直接访问的。

cpp 复制代码
class test
{
public:
	int add()
	{
		return _x + _y;
	}
private:
	int _x;
	int _y;
};

限定符说明

1)限定符的作用域是从该限定符开始到下一个限定符,若没有下一个限定符,则直接到类结束;

2)class的访问权限中,public是公有,private和protected是私有;

3)struct中默认访问权限是public。class中默认访问权限是private;

补充

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

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,对外公开接口来和对象进行交互。封装的本质是更好的管理,让用户更方便的使用类。


类的实例化

用类模板定义对象的操作叫做类的实例化。

cpp 复制代码
class test
{
public:
	int add()
	{
		return _x + _y;
	}
private:
	int _x;
	int _y;
};

int main()
{
	test s1;  //类模板的实例化,s1就是类对象
	return 0;
}

定义出的类实际上是不会分配空间存储的,而类对象则需要空间进行存储;类就是图纸,而类对象就是依据图纸得到的产品。

类对象的大小

类是没有大小的,只有在类模板实例化后才有大小,类对象的大小是其成员变量经过内存对齐之后的大小。注意类函数是不加到类对象的大小,类函数是类中公有的,创建对象是不需要为其开辟空间,需要调用的时候直接去其地址处调用即可,就好比自己家和小区篮球场的关系,需要的时候直接去使用,自己不用再为其开辟空间。

注意:没有成员变量的类对象也需要1个字节的大小进行占位。


类中隐藏的this指针

this指针的引入

cpp 复制代码
class test
{
public:
	void Init(int x, int y)
	{
		_x = x;
		_y = y;
	}
private:
	int _x;
	int _y;
};

int main()
{
	test t1;
	test t2;
	t1.Init(1, 2);
	t2.Init(3, 4);
	return 0;
}

++上述两个test的类对象,在调用Init的时候,编译器是怎么将其分开的,在调用的时候,编译器怎么知道对t1还是t2进行处理的??++

C++中引入了隐藏的this指针来解决这一问题。如上图所示,t1和t2都有其this指针分别是&t1和&t2,通过这种方式来区分是哪一个对象在调用类函数。

this指针的特性

1)this指针是不能显示调用的,但是在类函数内部是可以显示使用的;

cpp 复制代码
class test
{
public:
	void Init(int x, int y)
	{
		this->_x = x;  //其本意就是,_x=x;
		this->_y = y;
	}
private:
	int _x;
	int _y;
};

2)this指针实际上是,成员函数的形参,当调用成员函数的时候,将对象地址作为实参传递给this形参。所以对象中是不储存this指针的。

3)this指针作为形参不需要用户自己传,编译器会自动传递。


类的默认成员函数

类为了解决每次实例化对象后都要调用构造,开辟空间,出作用域又要销毁等问题,类设置了6个默认成员函数,其可以由编译器自动调用。


构造函数

构造函数是为了解决忘记对类对象进行初始化的问题。构造函数是一种特殊的类成员函数;

特征

1)函数名与类名相同;

2)无返回值;

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

4)构造函数可重载。

5)如果类中没有显示写构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器就不会再生成了。对于编译器生成的默认构造函数,内置类型不做处理,自定义类型会去调用其本身的默认构造函数。

cpp 复制代码
class test
{
public:
	test(int x =10,int y=20)
	{
		_x = x;
		_y = y;
	}

private:
	int _x;
	int _y;
};

构造函数也是函数,所以根据不同需求可以对构造函数进行重载。

什么时候要自己写构造函数呢??++一般情况下类成员中由内置类型的成员就需要写默认构造函数,全是自定义类型成员时,就不需要写构造函数,让编译器自己生成。++

缺省值

为了解决对内置类型,编译器不处理问题,可以自己写构造函数,也可以提供缺省值让编译器生成默认构造函数时使用。

cpp 复制代码
class test
{

private:
	int _x =10;
	int _y =20;
};

通过给_x和_y缺省值,编译器在调用默认构造时就会将其初始化。

默认构造函数

默认构造函数实际上分为3种:1)编译器默认生成的;2)全缺省的,有缺省值且每个形参都有;3)无参数的,构造函数没有参数。


析构函数

析构函数是对类对象中资源的销毁,大多数是进行动态内存的销毁。

特征

1)析构函数,函数名前加~;

2)无参数,无返回值;

3)对象的生命周期结束时,系统会自动调用析构函数。

4)一个类只能有一个析构函数,若未定义,系统会自动生成默认的析构函数,析构函数不能重载;默认析构函数对内置类型不处理,对于自定义类型去调用其自己的析构函数。

cpp 复制代码
class test
{
public:
	~test()
	{
		//销毁
	}
private:

};

什么时候需要使用析构函数??一般情况下,有动态开辟的空间,要显示写析构函数,当需要释放的资源都是内置类型的,不写析构函数。


拷贝构造函数

特征

1)拷贝构造函数是析构函数的一个重载;

2)拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错,++因为将实参赋值给形参的时候需要调用拷贝构造完成,这就会导致死循环的调用拷贝构造。++

3)但不写拷贝构造函数时,编译器会自动生成默认拷贝构造,默认拷贝构造对内置类型按照其字节数进行拷贝,对自定义类型会调用其默认的拷贝构造函数。内置类型的这种拷贝也被称为浅拷贝或值拷贝。如果类中有指针,对指针进行浅拷贝的时候就会导致,两个类对象指向同一块空间。

cpp 复制代码
class Date
{
public:
	//默认构造函数
	Date(int year = 1, 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;
};

运算符重载

C++为增强代码的可读性,引入了运算符重载,运算符重载是有特殊函数名的函数,其也有返回值和类型。关键字operator后面接需要重载的运算符符号即可。

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

cpp 复制代码
bool operator <(const Date& d)
{
	//有隐藏的this指针,_year,_month,_day又属于this指针指向的类对象
	if (_year < d._year)
		return true;
	if (_year == d._year && _month < d._month)
		return true;
	if (_year == d._year && _month == d._month&&_day<d._day)
		return true;

	return false;
}

以上对<的运算符重载,就可以直接实现Date类对象的比较。

要求

1)不能用一些特殊符号作为操作符,eg:@,#等;

2)重载操作符必须有一个类类型参数;

3)用于内置类型的运算符,其含义不能变,eg:重载+后还是加;

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

5).* :: sizeof ?:(三目) . 这5个关键字不能重载。

赋值运算符重载

赋值运算符重载是对两个已经初始化后的类对象进行赋值。

cpp 复制代码
class Date
{
public:
	bool operator =(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	private:
	int _year;
	int _month;
	int _day;
};

以上赋值运算符重载就可以实现对不同类对象间的赋值。


日期中的运算符重载

构造函数

cpp 复制代码
class Date
{
	//默认构造函数
	Date(int year=1, 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;
};

日期的比较

在设置运算符重载的时候,可以直接看出运算符之间的关系,从而直接调用其他已经实现的运算符来设置新的运算符。

cpp 复制代码
bool operator <(const Date& d)
{
	if (_year < d._year)
		return true;
	if (_year == d._year && _month < d._month)
		return true;
	if (_year == d._year && _month == d._month && _day < d._day)
		return true;

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

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

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

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

日期+天数:+=

通过日期加天数来获取未来的某一天的日期。

cpp 复制代码
int Getmonthday(int year, int month)
{
	int monthday[13] = { 0,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;
	return monthday[month];
}
Date& operator+=(int day)
{
	_day += day;
	int nday = Getmonthday(_year, _month);
	while (_day > nday)
	{
		_day -= nday;
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
		nday = Getmonthday(_year, _month);
	}
	return *this;
}

日期+天数:+

cpp 复制代码
Date operator+(int day)
{
	Date tmp(*this);  //通过拷贝构造一个新类对象进行返回
	tmp._day += day;
	int nday = Getmonthday(tmp._year, tmp._month);
	while (tmp._day > nday)
	{
		tmp._day -= nday;
		tmp._month++;
		if (_month == 13)
		{
			tmp._year++;
			tmp._month = 1;
		}
		nday = Getmonthday(tmp._year, tmp._month);
	}
	return tmp;
}

日期-天数:-=

cpp 复制代码
Date& operator-=(int day)
{
	_day -= day;
	_month--;  //向将month-1,向前加日期
	int nday = Getmonthday(_year, _month);
	while (_day < 0)
	{
		_day += nday;
		if (_day > 0)
			break;
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		nday = Getmonthday(_year, _month);
	}
	return *this;
}

日期前置++和后置++

cpp 复制代码
//前置++
Date& operator++()
{
	(*this) += 1;  //直接调用+=
	return *this;
}
//后置加加
Date operator++(int)  //为了与前置++分开,在参数中给一个int类型参数
{
	Date tmp(*this);
	*this += 1;

	return tmp;
}

日期-日期

通过日期-日期来获得两个日期间的天数,日期-日期可以直接用计数器,每次+1看加了多少次。

cpp 复制代码
//日期-日期
int operator-(Date d)
{
	Date max(*this);
	Date min(d);

	if (max < min)
	{
		max = min;
		min = *this;
	}
	int n = 0;
	while (min != max)
	{
		min++;  //直接用计数,每次++,看加多少次
		n++;
	}
	return n;
}

内存管理

new和delete

C++中引入新的关键字new和delete来对动态内存进行管理。

C++中申请和释放单个内存空间用new和delete,多个内存空间用new[ ]和delete[ ] 。

cpp 复制代码
int* pt1 = new int;     //开辟一个int的动态内存空间
delete pt1;

int* pt2 = new int(10); //开辟并初始化为10 
delete pt2;

int* pt3 = new int[10]; //开辟10个int类型的空间
delete[] pt3;

int* pt4 = new int[10] {1,2,3,4,5};  //开辟并依次初始化
delete[] pt4;

new对比malloc的优势是:对于自定义类型new会顺便调用类的构造函数,不需要我们手动再进行初始化了,同样delete对于自定义类型也会调用析构函数。

了解:C++是面向对象的语言,所以new失败后,不会像C语言一样用NULL返回值,C++更喜欢抛异常。

与malloc和free的区别

1)new和delete会对自定义类型调用构造和析构函数;

2)new和delete是关键字,malloc和free是函数;

3)new不用走动计算开辟空间的大小,malloc要手动计算;

4)new可以直接返回要开辟类型对应的指针,malloc返回的是void*还要强转;

5)开辟空间失败,new是抛异常,而malloc是返回NULL。

定位new

对于已经开辟好的空间,可以使用定位new来对空间调用析构函数。

定位new参与内存池搭配使用。用法如下。

cpp 复制代码
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};


int main()
{
	A* pa = (A*)malloc(sizeof(A));
	new(pa)A(1);//new(空间地址)自定义类型(传给构造函数的参数)
	return 0;
}

补充

cout和cin

cout和cin之所以能够实现类型的自动匹配,因为cout和cin库中有其对应的各个类型的重载函数,实际上也可以用运算符重载实现用cout和cin处理自定义函数。

此时就不能再间重载函数放在类中定义了,为什么???++因为类中有隐含的this指针,当还是在类中定义重载,就会导致需要d1<<cout这样使用输出,而我们希望的是cout<<d1来使用,所以就要在类外面定义重载函数。但是在外面定义重载函数就不能使用类的私有成员了,此时要通过声明友元函数来实现对类私有成员的访问。++

友元函数就是在类中声明自定义函数,其前面再加上friend;

Date的cout和cin

istream和ostream分别是cout和cin的类型。

cpp 复制代码
class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);

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

using std::ostream;
using std::istream;
using std::endl;
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
}
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
}

初始化列表

在C++中类的有些成员变量是必须在初始化列表中初始化,在构造函数中是无法完成初始化的,初始化列表实际上是构造函数的一部分,是类对象成员定义的位置。

必需在初始化列表中初始化

1)引用成员变量;

2)const修饰的成员变量;

3)没有默认构造的自定义类型成员变量。

cpp 复制代码
class test
{
	test(int& x)
		:a(x)   //将a引用为x
		,b(2)   //将b赋值为2
	{}

private:
	int& a;
	const int b;
};

注意:在初始化列表中初始化的顺序不是按照代码顺序进行的,而是按照类成员声明的顺序向下进行的。

静态成员变量

类中的静态成员变量和普通成员变量的主要区别在于:普通成员变量属于每个类对象,储存在对象里面,而静态成员变量属于类,是公有的存储在静态区。

静态成员需要在类中声明,在类外定义。

cpp 复制代码
class test
{
private:
	int a;
	int b;
	static int st;
};

int test::st = 0;

静态成员函数

静态成员函数与普通成员函数的区别是,其没有隐藏的this指针,所以调用静态成员函数要用类域和访问限定符实现。同时因为没有this指针,其不能访问类中的私有成员及其他的非静态成员函数。

匿名对象

当我们只是临时需要创建一个对象,只要求在这一行使用该对象时,就可以创建匿名对象。

cpp 复制代码
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};

int main()
{
	A aa(1);  //有名对象
	A(1);     //匿名对象
	return 0;
}

有名对象的生命周期是当前函数局部域,匿名对象的生命周期是当前行。

补充:匿名对象和临时变量一样,具有常性,引用时要带const修饰,当对匿名对象进行引用时,可以延长匿名对象的生命周期,生命周期变为当前函数局部域。

模板,泛型编程

在C语言中有时候因为类型不同要写多个重复的函数,C++为了解决这一问题引入了模板,泛型编程的概念。

泛型编程:编写与代码类型无关的通过代码,是代码复用的一种手段。模板是泛型编程的基础。

函数模板的格式:template<typename T1,typename T2...typename Tn>

返回值 函数名(参数列表){ }

cpp 复制代码
template<typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

以上代码就是交换函数的模板,T相当于可以自动识别各个类型。typename是定义模板参数的关键字,也可以用class。

注意:函数模板实际上只是一张蓝图,其本身不是函数;在编译器编译阶段,编译器首先会识别实参的类型来推演生成对应类型的函数以供调用。

对于类模板来说,类名和类型是不一样的。eg:stack是类名,stack<int>是类型。

模板参数匹配原则

1)一个非模板参数可以和一个同名的模板参数同时存在,并且该模板参数可以实例化为该非模板参数;

cpp 复制代码
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

template<typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 10, b = 20;
	Swap<int>(a, b);  //让模板函数实例化为与会模板函数相同的函数
	return 0;
}

2)对于非模板函数和同名的模板函数来说,如果其他条件都相同,在调用时会优先调用非模板函数。当模板函数能够实例出一个更匹配的,则调用模板函数。

cpp 复制代码
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

template<typename T1,typename T2>
void Swap(T1& x, T2& y)
{
	T1 tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 10, b = 20;
	double c = 1.1;
	Swap(a, b);   //调用非模板函数
	Swap(a, c);   //调用模板函数
	return 0;
}
相关推荐
我不会编程55515 小时前
Python Cookbook-5.1 对字典排序
开发语言·数据结构·python
李少兄15 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
懒羊羊大王&15 小时前
模版进阶(沉淀中)
c++
无名之逆15 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
似水এ᭄往昔15 小时前
【C语言】文件操作
c语言·开发语言
啊喜拔牙15 小时前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala
owde16 小时前
顺序容器 -list双向链表
数据结构·c++·链表·list
xixixin_16 小时前
为什么 js 对象中引用本地图片需要写 require 或 import
开发语言·前端·javascript
GalaxyPokemon16 小时前
Muduo网络库实现 [九] - EventLoopThread模块
linux·服务器·c++
W_chuanqi16 小时前
安装 Microsoft Visual C++ Build Tools
开发语言·c++·microsoft