C++——类和对象

目录

定义类

成员函数有两种定义方式:

成员变量名的建议:

访问限定符

类的作用域

类的实例化

类对象的存储方式

this指针

this指针的特性

this指针可以为空吗

类的默认成员函数

构造函数——初始化

初始化列表

默认构造函数

构造函数的调用

析构函数——成员销毁(析构是按照构造的相反顺序进行析构,static变量最后销毁)

默认析构函数

拷贝构造函数

默认构造拷贝函数

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

赋值运算符重载

运算符重载

赋值运算符重载是类成员函数

前置++和后置++

<< 和 >> 重载

const修饰成员函数

explicit修饰构造函数

static成员

定义

特性

友元函数


定义类

//class是定义类的关键字    name是类名
class name
{
    //类体:由成员函数(类方法)和成员变量(类属性)组成
};

 C语言中的结构体只能定义变量,但是在C++中还可以定义函数,这是因为C++将结构体上升为类

struct Person
{
	char _name[10];
	int _age;
	char _sex[5];
	int _score;
	void ShowInfo()
    {
	cout << _name << endl; 
    cout << _sex << endl; 
    cout <<_age << endl; 
    cout << _score << endl;
    }
};

成员函数有两种定义方式:

  • 声明和定义全部放在类体中(函数可能被编译器当作内联函数处理)
  • 类的定义和函数声明放在的头文件中,函数的定义放在源文件中,成员函数名前要+类名和" :: "
//Person.h
class Person
{
	char _name[10];
	int _age;
	char _sex[5];
	int _score;
	void ShowInfo();
};
//Person.cpp
void Person::ShowInfo()
    {
	cout << _name << endl; 
    cout << _sex << endl; 
    cout <<_age << endl; 
    cout << _score << endl;
    }

成员变量名的建议:

class date
{
private:
	int _day;    
	int _mouth;    
	int _year;    
public:
	void Year(int year)
	{
		_year = year;    
	}
	void Mouth(int mouth)
	{
		_mouth = mouth;    
	}
	void Day(int day)
	{
		_day = day;    
};

函数的形参与成员变量同名,赋值时很难分清二者是谁,所以推荐在成员变量名加修饰


访问限定符

fd6ad1ac9b264eee91e040c4cea11c89.png

  • public修饰的成员可以在类外被访问
  • privata和protected修饰的成员不可在类外被访问
  • 访问限定符的作用域是从此限定符开始到下一个限定符结束,如果后面没有访问限定符,那么到类的 } 结束
  • 为了和C兼容,struct被升级为类后,所有的成员默认都是public;class默认都是private

类的作用域

类成员所在的区域都在类的作用域,当在类外定义成员函数时,需要使用 :: (作用域限定符)指明成员属于哪个作用域

class date
{
private:
	int _day;
	int _mouth;
	int _year;
public:
	void Year(int year);
};

void date::Year(int year)
{
	_year = year;
}

类的实例化

创建类对象的过程就是类实例化的过程

把类比作一张建筑模型图,类的实例化就像按照图纸完成这个建筑的建造。类就是对对象的描述,在内存中并没有分配空间

一个类可以创建多个对象,每个对象都会占用空间

class date
{
private:
	int _day;    
	int _mouth;   
	int _year;    
public:
	void Year(int year)
	{
		_year = year;
	}
	void Mouth(int mouth)
	{
		_mouth = mouth;
	}
	void Day(int day)
	{
		_day = day;
	}
	void Show()
	{
		cout << _year << endl << _mouth << endl << _day << endl;
	}
};

int main()
{
	date today;
	today.Year(2023);
	today.Mouth(4);
	today.Day(21);
	today.Show();
	return 0;
}

类对象的存储方式

class date
{
private:
	int _day;    
	int _mouth;   
	int _year;    
public:
	void Year(int year);
	void Mouth(int mouth);
	void Day(int day);
	void Show();
};

int main()
{
	date today;
	cout << sizeof(today) << endl; //12
	return 0;
}

类对象只保存成员变量,成员函数保存在公共代码段,不计入对象的大小。所以类成员大小就是成员变量按照内存对齐原则计算得到的结果

如果一个类没有成员变量,那么实例化的成员大小是1字节,只是为了占位,表示对象存在,不存储有效数据。


this指针

既然成员函数不存在对象的空间中,那当不同的对象调用相同的函数时,函数是怎么识别具体对哪一个对象做出改变的呢?原来,C++编译器为每一个成员函数增加了一个隐藏的指针参数,让指针指向现在调用此函数的对象,对象的改变都是通过对这个指针操作完成的,只不过传参的过程对用户是透明的,由编译器自动完成

this指针的特性

  1. this指针的类型:类类型* const,成员函数不能对this赋值
  2. 只能在函数内部使用
  3. this指针的本质是成员函数的形参(存储在栈中),当对象调用函数时,会将对象的地址作为实参传递给this,所以对象中不存储this形参
  4. this指针时成员函数的第一个隐藏形参,一般情况由编译器通过ecx寄存器传递,不需要用户传递

this指针可以为空吗

当我们通过对象调用函数时,this一定不为空;当用指针调用函数时可能为空

class date
{
private:
	int _day;    
	int _mouth;   
	int _year;    
public:
	void Show()
	{
		cout << "Show()" << endl;
	}//函数未使用this访问成员变量,this是空指针也可以运行

	void Show()
	{
		cout << _year << endl;    //this->_year
	}//函数通过this访问了成员变量,对空指针的访问会发成错误
};

int main()
{
	date* today = nullptr;
	today->Show();
	return 0;
}

类的默认成员函数

用户没有显示实现,编译器会生成的成员函数叫默认成员函数

class data{};

像这样,一个类中什么都没有的类叫空类,那空类中什么都没有吗?当然不是,编译器会自动生成一些默认成员函数:

b2a77bcaf87643f5ad3bd42607bc2379.png

 常用的有上面四种:


构造函数——初始化

构造函数是一种特殊的成员函数,函数名与类名相同创建类的对象时自动调用,使对象的成员变量有合适的初始值,并且在对象的生命周期内只能调用一次

特点:

  • 函数体前有初始化列表用来初始化
  • 函数名与类名相同
  • 无返回值(不是void)
  • 对象实例化时编译器自动调用构造函数
  • 构造函数可以重载

初始化列表

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

注意:

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(类没有默认构造函数时必须初始化,如果有默认构造会自动调用可以不写)

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

成员变量初始化只在初始化列表中完成,类中的成员变量只是声明,声明变量给的默认值就是初始化列表的缺省值。函数体内可以为变量赋值,但不是初始化。

class Stack
{
private;
    //只是变量的声明
    int _capacity = 40;
    int _top = 0;
    int* _a;
public:
    Stack()
    //初始化列表
    :_a( (int*)malloc(sizeof(int)*_capacity) )
    ,_top(0)
    ,_capacity(capacity)
    
    //函数体
    {
        if(nullptr == _a)
        {
            perror("malloc fail\n");
        }
    }
};

默认构造函数

用户不定义实现构造函数的情况下,编译器会自动生成默认构造函数(无参)。C++规定自动生成的默认构造函数不对内置类型(基本类型)初始化,对于自定义类型成员,会调用它的成员构造函数。

注:

C++11 中针对内置类型成员不初始化的缺陷,做了优化,即:内置类型成员变量在类中声明时可以给默认值,这其实是初始化列表中成员变量的缺省值。

构造函数的调用

1.无参构造函数

class date
{
private:
	int _day;    
	int _mouth;   
	int _year;    
public:
	date()
    :_day(1)
    ,_mouth(1)
	_year(2023)
	{}
};

int main()
{
	date today;
	return 0;
}

//注意 :
//当构造函数无参或者是全缺省参数时,对象实例化调用构造函数时不需要加"()",
//因为: date today(); 也可以认为是函数声明,
//编译器无法辨别

2.全缺省构造函数

class date
{
private:
	int _day;    
	int _mouth;   
	int _year;    
public:
	date(int day = 1, int mouth = 1, int year = 2023)
	{
		_day = day;
		_mouth = mouth;
		_year = year;
	}
};

int main()
{
	date today;
	return 0;
}

3.带参构造函数

class date
{
private:
	int _day;    
	int _mouth;   
	int _year;    
public:
	date(int day, int mouth, int year = 2023)
    :_day = day
	,_mouth = mouth
	,_year = year
	{}
};

int main()
{
	date today(22, 4, 2023);
	return 0;
}

//注意:
//使用 data today; 会报错,因为用户定义了构造函数
//编译器不会生成无参的默认构造函数,对象实例化时,
//必须要给初始值

 无参构造函数和全缺省构造函数不能同时定义,虽然它们构成函数重载,但是调用它们时,函数的实例与多个形参列表匹配,造成对重载函数调用不明确的问题


析构函数——成员销毁(析构是按照构造的相反顺序进行析构,static变量最后销毁)

特点:

  • 析构函数名是在类名前加~
  • 析构函数无参无返回值类型
  • 析构函数未定义,会自动生成。析构函数不能重载
  • 对象的生命周期结束,系统会自动调用析构函数

默认析构函数

用户没有定义析构函数,系统自动生成的析构函数,它对内置类型不做处理,最后由系统统一回收即可;对自定义类型,会调用它自己的析构函数

class Data
{
private:
	int _year;
	int _mouth;
	int _day;
public:
	Data(int year = 2023, int mouth = 0, int day = 0)
	{
		_year = year;
		_mouth = mouth;
		_day = day;
		cout << "Data()" << endl;
	}

	~Data()
	{
		cout << "~Data()" << endl;
	}
};
class Stack
{
private:
	int* arr;
	int capacity;
	int top;
	Data today;
public:
	Stack(int init_capacity = 4)
	{
		arr = (int*)malloc(sizeof(int) * init_capacity);
		if (arr == NULL)
		{
			perror("malloc fail");
			return;
		}
		capacity = init_capacity;
		top = 0;
		cout << "Stack()" << endl;
	}

	~Stack()
	{
		free(arr);
		top = 0;
		capacity = 0;
		cout << "~Stack()" << endl;
	}
};
int main()
{
	Stack s;
	return 0;
}
//运行结果:
//Data()
//Stack()
//~Stack()
//~Data()

拷贝构造函数

特点:

  • 拷贝构造函数是构造函数的重载函数
  • 拷贝构造函数的参数只有一个,且是该类类型的引用(使用传值会造成无穷递归)

无穷递归的原因:

C++规定,内置类型传参为浅拷贝(直接传值),自定义类型传参是深拷贝。所以第一次调用拷贝构造时,实参与形参间会调用拷贝构造,而这个构造的形参的深拷贝还会再次调用构造,依此类推无穷无尽。

默认构造拷贝函数

未显式定义,系统自动生成的默认拷贝构造函数,对内置类型会进行浅拷贝(值拷贝),对自定义类型会调用自己的拷贝构造函数

class Data
{
private:
	int _year;
	int _mouth;
	int _day;
public:
	Data(int year = 2023, int mouth = 0, int day = 0)
	{
		_year = year;
		_mouth = mouth;
		_day = day;
	}
};
int main()
{
	Data d1(2023, 4, 26);
	Data d2(d1);
	return 0;
}

既然系统自动生成的拷贝构造函数可以对内置类型拷贝,我们还有必要写吗?当然有必要了,看下面的例子:

class Stack
{
private:
	int* _arr;
	int _capacity;
	int _top;
public:
	Stack(int capacity = 4)
	{
		_arr = (int*)malloc(sizeof(int) * capacity);
		_capacity = capacity;
		_top = 0;
	}

	Stack(const Stack& s)
	{
		_arr = (int*)malloc(sizeof(int) * s._capacity);
		_capacity = s._capacity;
		_top = s._top;
	}

	~Stack()
	{
		free(arr);
		top = 0;
		capacity = 0;
		cout << "~Stack()" << endl;
	}
};
int main()
{
	Data d1(2023, 4, 26);
	Data d2(d1);
	return 0;
}

Stack的成员_arr是指针类型,初始化d1时_arr向内存动态申请了空间。如果不写拷贝构造函数,在拷贝时不会为新对象d2的_arr分配一块空间,而是把d1的内容浅拷贝给d2,在对象生命周期结束后,析构函数会对指向同一块空间(d1的_arr和d2的_arr)的指针做两次内存释放,就会造成错误。

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

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象

函数形参对实参的拷贝会调用拷贝构造函数

  • 函数返回值类型为类类型对象

函数要返回的类对象是由拷贝构造函数拷贝到另一个类对象的

例子:

class Date 
{
private:
	int _year;
	int _mouth;
	int _day;
public:
	Date(int year = 2023, int mouth = 1, int day = 1)
	{
		_year = year;
		_mouth = mouth;
		_day = day;
		cout << "Data(int int int)" << endl;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_mouth = d._mouth;
		_day = d._day;
		cout << "Date(const Data& d)" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
};

Date copy(const Date d)
{
	Date tmp(d);
	return tmp;
}

int main()
{
	Date d(2023, 5, 1);
	copy(d);
	return 0;
}

//运行结果:
//Data(int int int)
//Date(const Data& d)
//Date(const Data& d)
//Date(const Data& d)
//~Date()
//~Date()
//~Date()
//~Date()

分析:

1.创建Date的对象会调用构造函数

2.调用copy函数时,传参会调用拷贝构造函数

3.copy函数内创建一个对象,通过调用拷贝构造函数来初始化

4.copy函数返回的是一个临时Date对象,该对象是用tmp通过拷贝构造函数构造的


赋值运算符重载

运算符重载

运算符重载是具有特殊函数名的函数,函数名为:operator+要重载的运算符符号

函数格式:返回值类型 operator运算符 (参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*(指向成员的指针运算符)   ::(域作用限定符)   sizeof   ?:    .   以上5个运算符不能重载

class Date 
{
	friend bool operator== (const Date& d1, const Date& d);//友元函数
	int _year;
	int _mouth;
	int _day;
};

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

int main()
{
	Date d(2023, 5, 1);
	Date p(d);
	//cout << operator==(d, p) << endl;
    cout << (d == p) << endl;
    //两种调用方法都可以
	return 0;
}

因为成员变量是私有的,在类外无法访问,为了保证类的封装性,不能将成员变量设置为公有,可以使用友元函数解决,本文会在后面详细介绍友元函数,但是友元函数会破坏封装性,我们一般会将运算符重载写到类中。

class Date 
{
private:
	int _year;
	int _mouth;
	int _day;
public:
    bool operator== (const Date& d)
    {
	    return _year == d._year && _mouth == d._mouth && _day == d._day;
    }
};

赋值运算符重载是类成员函数

格式:

  1. 参数使用类的引用类型
  2. 参数返回类型使用类的引用类型
  3. 检查是否自己给自己赋值
  4. 返回 *this
class Date 
{
private:
	int _year;
	int _mouth;
	int _day;
public:
	Date& operator=(const Date& d)
	{
		if (this != &d) 
		{
			_year = d._year;
			_mouth = d._mouth;
			_day = d._day;
		}
		return *this;
	}
};

注意:

  • 赋值运算符重载必须写成成员函数,因为如果赋值运算符重载没有在类内显式实现,系统会自动生成,就会和类外的重载函数冲突。
  • 对于内置类型函数是直接赋值的,对于自定义类型会调用其赋值运算符重载函数
  • 系统生成的默认赋值运算符重载函数,只会以值的方式逐字节拷贝,和拷贝构造函数类似,当遇到动态内存管理时,就会发生错误,必须要用户自己实现函数

前置++和后置++

//这段代码只是演示++的重载,真正意义实现日期类的++稍显复杂,这里不做演示
class Date 
{
private:
	int _year;
	int _mouth;
	int _day;
public:
	//前置++
	Date& operator++()
	{
		_day++;
		return *this;
	}
	//后置++
	Date operator++(int)
	{
		Date tmp(*this);
		_day++;
		return tmp;
	}
    //tmp是临时对象出了函数就会销毁,不能返回引用
};

前置++和后置++都是一元操作符,为了区分重载函数,C++规定:后置++多加一个int类型的参数,但是调用函数时由系统自动传递。

<< 和 >> 重载

class Date
{	
private:
    int _year;
    int _month;
    int _day;
public:
    friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
};
		
ostream& operator<<(ostream& out, const Date& d)
{
	if (d._month <= 0 || 
    d._month > 12 || 
    d._day <= 0 ||     
    d._day > Date::GetMonthDay(d._year,d._month))
	{
		out << "日期错误" << endl;
	}
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	cin >> year >> month >> day;
	if (month <= 0 || month > 12 || day <= 0 || day > Date::GetMonthDay(year, month))
	{
		cout << "输入错误" << endl;
	}
	d._year = year;
	d._month = month;
	d._day = day;
	return in;
}

int main()
{
    Date d1;
    Date d2;
    cin << d1 << d2;
    cout << d1 << d2;
    return 0;
}

<<和>>操作符的重载一般有两种形式:

  1. 在类外定义,把它设置为友元函数。因为类内定义,函数的第一个参数只能是隐含的this指针,调用时只能是d.operator<<(cout)或者d<<cout,这样看起来非常奇怪,不符合使用习惯
  2. 在类外定义,类内设置公共的get方法访问成员变量。

const修饰成员函数

const修饰成员函数,const实际上是对函数第一个参数this的修饰,使得函数不能对类的成员做修改。

void Print()const ->void Print(const Date* this)

建议将所有不需要改变类成员的函数都使用const修饰,因为const修饰的参数可以同时接收被const和不被const修饰的成员


explicit修饰构造函数

当构造函数只有一个参数或者除了第一个参数都是缺省参数时,用一个这个参数类型的变量给对象赋值会发生类型转换:编译器先用变量创建一个匿名对象,再用匿名对象赋值

class Date
{
private:  
    //...
public:
    Date(int n)  //Date(int n1,int n2 = 0,...,int nn = 0)
    {
        //...
    }
};

int main()
{
    Date d = 10;
    //Date d = Date(10);
}

但是,如果单参构造函数被explicit修饰,就没有了类型转换的作用,这样的代码就不会通过。


static成员

定义

用static修饰的成员变量称为静态成员变量,静态成员函数一定要在类外初始化;用static修饰的额成员函数称为静态成员函数。

特性

  • 静态成员为所有类对象共享,不属于某个具体对象,存放在静态区
  • 静态成员可以用类名::静态成员 或者 对象名.静态成员访问
  • 静态成员也是类成员,受访问限定符的限制
  • 静态成员变量必须在类外定义,定义时不用写static关键字,类中只是声明
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

友元函数

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

前面在写<< >>运算符重载时,要使用友元函数,就是因为类成员函数第一个参数会被this指针占用,而在<< >>的使用中,习惯上将cout和cin放在第一个参数的位置上。


匿名对象

对象创建时没有对象名,直接给出初始化列表的对象就是匿名对象。

注意:

  • 无参构造,类名必须加括号,要与非匿名对象创建区分
  • 匿名对象即用即销毁,它的作用域仅在当前行
  • 匿名对象赋值给引用,会延长该对象的生命周期,Date& pd = Date d()
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值