C++:类和对象

目录

一、类的基础

1.类 class/struct

2.类外定义

3.对象的实例化

4.隐含的this指针

二、类的6个默认成员函数

1.构造函数

2.析构函数

3.拷贝构造函数

4.运算符重载

 赋值运算符重载

关系运算符重载

 前置运算符和后置运算符

流输入和流提取

5.取地址重载  和  const取地址重载

三、其他

1.友元函数和友元类

2.静态成员变量

3.初始化列表

4.内部类

5.匿名对象

6.构造对象时的一些优化


一、类的基础

1.类 class/struct

类可以理解为c语言中结构体的升级版,在类中不仅仅能够存储数据了,还可以写函数,在类中我们称之为成员变量(数据)和成员函数(函数), 一个类可以实例化出N个对象,类名即可表示对象的数据类型class/struct + 类名 { ... };即为类的表示(注意最后有一个分号)

类内都有访问限定符:public,protected,private

public修饰的在类外可以直接访问,protected和private不可以,这两个更多的区别会在后面讲到,class的默认访问限定符是private,而struct默认的是public,所以明显class的封装性是更好的,更加安全,所以我们一般习惯使用的是class更多

class A  //类A
{
    //默认private,在最上面修饰可以不说明
    int a;
    int b;  //成员变量
    ...  
public:
    void ptint() { ... }  //成员函数
    ...
protected:
    ...
private:
    ...  //放在下面要强调是private的
};
A a1;  //一个类A的对象a1

在声明成员变量的时候可以给缺省值,int day = 1; 

2.类外定义

成员函数可以在类的外面再定义,类内先声明,但是在类外定义要注意用域作用限定符标注函数的作用域

class Stack
{
public:
    void Init();  //声明
};
void Stack::Init()  //定义
{
    ...
}

3.对象的实例化

类内的成员都属于声明,没有实例化,比如上面的那个类Stack,需要 Stack st;此时才属于定义出了一个成员对象

计算一个类的大小:sizeof(st)  <=>  sizeof(Stack), 二者等价,可以是对象名或者类名

在类的大小中,成员函数不算入栈帧内存大小,成员变量才算,类中的成员函数定义完之后就是有一个固定的地址,每个类对象调用的成员函数地址都是一样的,因为如果类内每个函数都存会造成很大的浪费

可以发现空类的大小为1,而且成员函数的内存确实不计入类的内存大小 

4.隐含的this指针

在类内有一个隐含的this指针,指向的是成员对象,eg. d.Print();  <=>  d1.Print(&d);

1.this指针在实参和形参是不可以显示的写出来的,编译器自己添加

2.在类内可以显示的使用this指针

#include <iostream>
using namespace std;
class A{
	int n = 1;
public:
	void Print(){
    	cout << this->n << ' ' << n;
	}
};
int main(){
	A a;
	a.Print();
	return 0;
}
结果:1 1

由调试结果可知this指的对象就是a,二者的成员变量是一样的

3.this指针的储存地址是在栈上,因为this也是个形参

二、类的6个默认成员函数

1.构造函数

1.函数名和类名相同

2.无返回值,不写void

3.成员对象实例化时自动调用

4.可以函数重载

class Date
{
public:
	//无参的构造函数
	Date()
	{
		_day = 1;
	}
	//有参的构造函数
	Date(int day)
	{
		_day = day;
	}
	//全缺省的构造函数
	Date(int day=1)
	{
		_day = day;
	}
private:
	int _day;
};

 一般在构造函数内初始化成员变量,如果没有初始化,成员变量则为随机值,无参构造函数和全缺省构造函数同时出现时会报错,因为有歧义,所以不可以同时存在

成员对象在实例化时,编译器会自动匹配对应的构造函数

#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		_day = 1;
		cout << "Date()" << endl;
	}
	Date(int day)
	{
		_day = day;
		cout << "Date(int day)" << endl;
	}
private:
	int _day;
};
int main()
{
	Date a;
	Date b(10);
	return 0;
}
结果:Date()
     Date(int day)

 在没有手搓的构造函数时,编译器会自动生成无参构造函数

可见是可以的,但是绝大多数情况下都有初始化的需求所以都是需要手搓的

2.析构函数

1.形式:~类名

2.无参数,无返回值

3.对象生命周期结束时,自动调用,完成对象中的资源清理

4.不写时编译器会自动生成

#include <iostream>
using namespace std;
class Date
{
public:
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _day;
};
int main()
{
	cout << "1 ";
	Date a;
	cout << "2 ";
	return 0;
}
结果:1 2 ~Date()

这段代码中可以看出来析构函数的调用是在出了主函数,成员对象a要销毁时才调用的

3.拷贝构造函数

1.是构造函数的一种重载

2.参数为类类型对象的引用

3.不写时编译器会自动生成

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int day)
	{
		_day = day;
	}
    //拷贝构造函数
	Date(Date& d)
	{
		_day = d._day;
	}
private:
	int _day;
};
int main()
{
	Date a(10);
    //下面两个等价,都可以,都属于拷贝构造
	Date b(a);
    Date c = a;
	return 0;
}

当类类型的成员对象作为实参传递给形参,也就是传值传参的时候,会调用拷贝构造

#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{ }
	Date(Date& d)
	{
		cout << "Date(Date& d)" << endl;
	}
	void func(Date d)
	{
	}
private:
	int _day;
};
int main()
{
	Date a;
	Date b;
	b.func(a);
	return 0;
}
结果:Date(Date& d)

清楚了传值传参会调用拷贝构造后,我们就可以来看为什么一定要用类类型的引用???

 下面会给出一段代码,从这里可以知道为什么

void func(Date d)
{
    ...
}
Date d(10);
func(d);

这段代码中,成员对象d作为func函数的实参传过去的时候,会调用拷贝构造,而拷贝构造的形参又是一个类类型对象传值,Date(Date d),接下来就是无穷递归,所以必须要传址传参

以上的拷贝构造,为浅拷贝,是按字节拷贝的,但是在遇到栈的情况下会遇到问题,比如array1和array2同时要拷贝一个空间,那这个被拷贝的空间就会析构两次,导致程序崩溃,所以面对这种情况我们需要深拷贝,也就是去开一个一样大的空间,然后一个一个传值,下面的代码就是深拷贝的一个例子

Stack(const Stack& st)
{
	_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
	memcpy(_array, st._array, sizeof(DataType) * st._size);
	_size = st._size;
	_capacity = st._capacity;
}

4.运算符重载

形式:返回值 operator 重载符号 (参数)

1.参数个数看操作符的操作数,要注意类内的成员函数有this这个隐含的参数

2.返回值看需求,eg.比大小返回bool,日期类相减返回天数int

3.不能创建语言外的新符号,比如@

4.重载操作符必须有一个类类型的参数,int operator-(int, int)就不可以,因为编译器本来就有

5.最好不要修改符号本身的含义,比如“-”实现“+”,除非有需求

6.不能重载的5个运算符:sizeof,

                                        ::     域作用限定符,

                                        ?:条件运算符,

                                        .      点

                                        .*    点星

7.必须重载为成员函数的4个运算符:=     赋值

                                                           [ ]    下标运算符

                                                           ( )    括号

                                                           ->    箭头                

class Date
{
	int _year;
	int _month;
	int _day;
public:
    bool operator != (Date d)
    {
	    if (_year == d._year && _month == d._month && _day == d._day)
	    {
		    return false;
	    }
	    return true;
    }
};
 赋值运算符重载

一个已经存在的对象,拷贝赋值给另一个已经存在的对象

Date d1;     Date d2;     d2 = d1;   两个对象都要是已经存在的

区分:

拷贝构造:Date d1;    Date d2 = d1;    这里d2是原本不存在的对象,是一个已经存在的去赋值给一个要创建初始化的对象,这个是拷贝构造

1.返回值为类类型或引用,这样便于连续赋值,d1 = d2 = d3,从右往左,d2 = d3的返回值是d2,然后再d2赋值给d1,如果是void的话就不能连续赋值

2.不写时编译器会自动生成

Date& operator= (const Date& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}
关系运算符重载

多运用复用思想,比如先重载 < 和  == ,就可以用这两个写 <=,再重载 > 

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

运算符+不会改变自己,是产生新的对象,所以返回值是类,而+=是改变自己,所以返回值是类的引用,可以用+=复写+

多个同一运算符重载可以构成函数重载 

Date& operator - (int day)
{
    d1 += day;
    //表达的含义返回为日期d1在day天之后的日期
    return d1;
}
int operator - (const Date& d)
{
    int ret = d1 - d;
    //表达的含义为两个日期之间差多少天,返回值为天数,是整形
    return ret;
}

ps:但是一般两个日期差值用小日期++和大日期判断,整形变量计数方便一点
int count = 0;
while (max != min)
{
    ++min;
    ++count;
}
 前置运算符和后置运算符

为了能够更好的区分前置和后置,强制规定前置运算符无参数,后置运算符带一个参数int,这个int无意义,但是必须要写

Date& operator++()  //前置
{
    *this += 1;
    return *this;
}
Date operator++(int)  //后置
{
    Date tmp(*this);
    *this += 1;
    return tmp;
}

因为后置返回的是++之前的值,所以要用一个tmp储存起来,然后返回,因为返回的是一个临时对象,所以是传值返回

流输入和流提取

1.内置类型可以直接使用,但是自定义类型就需要重载

2.返回值为istream/ostream,用void不太好,也是一样为了便于连续输入

3.建议重载成全局函数或者友元函数,不然的话形参里面就会首先先默认占据一个隐含的this指针,在主函数内的顺序就变成 d << cout 了,非常的不符合逻辑

class Date
{
    int _year;
    int _month;
    int _day;
public:
    friend istream operator >> (istream& in, Date& d);
    friend ostream operator << (ostream& out, const Date& d);
};
istream operator >> (istream& in, Date& d)
{   
    in >> d._year >> d._month >> d._day;
    return in;
}
ostream operator << (ostream& out, const Date& d)
{
    out << d._year << ' ' << d._month << ' ' << d._day << endl;
    return out;
}

5.取地址重载  和  const取地址重载

1.取地址重载可以获取对象的地址,const取地址重载获取const对象的地址

2.不写时编译器会自动生成

class Date
{ 
public :
	Date* operator&()
	{
		return this ;
	}
	const Date* operator&()
	{
        return this ;
	}
private :
	 int _day ; 
};

三、其他

1.友元函数和友元类

友元函数:friend + 函数声明

1.这个是声明,友元函数的定义在类内的话直接定义,在类外的话不用加friend

2.友元函数的参数没有隐含的this指针

3.友元函数可以访问成员对象的私有成员变量

class Date
{
public:
	friend void Print(Date d)  //类内定义
	{
		cout << d._day;
	}
private:
	int _day = 1;
};
//类外定义
//void Print(Date d)
//{
//	  cout << d._day;
//}

 友元类:一个类中可以作为另一个类的成员变量,friend + class + 类名,就是友元类的声明

1.友元类可以访问外部类的成员

2.但是外部类不可以访问友元类的成员

class Time
{
    friend class Date;
    ...
};
Date就可以访问Time的成员,Time不可以访问Date的

2.静态成员变量

关键字:static

1.存在于静态区,不存在于对象中

2.声明时不能给缺省值

3.必须在类外定义

class A
{
private:
    int a1;
    int a2;
    static int count;
//不存在于对象中 -> sizeof(A)=8
};
int A::count = 0;

3.初始化列表

1.形式:构造函数名 :成员变量1(参数),成员变量2(参数),......

   初始化列表是每个对象中成员定义的地方

2.初始化列表的初始化顺序是按照类内成员变量声明的顺序来的,不是按照列表成员变量出现的顺序的

3.初始化列表的本质是调用的默认构造函数

4.初始化列表和函数体内初始化可以混用

5.在成员变量声明时给的缺省值就是在初始化列表调用的,由于静态成员变量不在对象中,所以不会走初始化列表,也解释了静态成员变量为什么不可以在声明时给缺省值

6.const和引用&必须写在初始化列表处,因为这两个必须在定义时就初始化

Date(int year, int month, int day): _year(year), _month(month)
{
    _day = day;  //可以混用
}

4.内部类

1.类内的类就是内部类,仅仅受到类域和访问限定符限制,不影响外部类的size

2.内部类天生为外部类的友元

3.内部类是外部类的私有

class A
{
    class B;
};

A a1;  //可以
B b1;  //不可以
A::B b2;  //可以

5.匿名对象

1.匿名对象具有常性

2.匿名对象的生命周期只有创建的那一行,使用完立马销毁 

class A
{
    ...
};
A(2);     //匿名对象
A a(1);   //有名对象

6.构造对象时的一些优化

1.类类型作为形参的时候,如果传值传参,会有两次构造两次析构,一次在传过去的时候会创建一个临时对象,一次在传返回值的时候又会创建一个临时对象,而引用的传址传参的话就只有一次,可以减少拷贝

2.全局对象是在main之前进行构造,析构则相反,在main之后

3.局部静态变量是在第一次使用的时候初始化,不使用则不会初始化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值