类和对象(上)

前言

类和对象(上)

1.类初始介绍

以前C语言中结构体中就只能引入变量,而现在C++中结构体中就可以引入函数了

typedef int DataType;
struct Stack
{
	void init()
	{
		a = NULL;
		capacity = 0;
		size = 0;
	}
	int* a;
	int capacity;
	int size;
};

但现在一般不用struct了,一般用class,这两个是一样的作用

typedef int DataType;
struct Stack
{
	void init()
	{
		a = NULL;
		capacity = 0;
		size = 0;
	}
	int* a;
	int capacity;
	int size;
};

这个Stack就是一个类,用类来创造的变量叫做对象,而且创造对象时不用写class,直接写Stack
在这里插入图片描述
这个Stack就是类,s1就是对象
函数调用如上
类里面的函数叫做成员函数,变量叫做成员变量

2.访问限定符

类里面有三个,public(公开可以访问),private(私有不可访问),和protected其中protected我们暂时不用
在这里插入图片描述
class默认是private,struct默认是public,所以这里会报错

class Stack
{
public:
	void init()
	{
		a = NULL;
		capacity = 0;
		size = 0;
	}
private:
	int* a;
	int capacity;
	int size;
};
int main()
{
	Stack s1;
	s1.init();
	return 0;
}

一个访问限定符的范围是到另一个访问限定符,如果没有另一个,就直接到末尾,没有说明是什么访问限定符,就是用系统默认的访问限定符
一般成员变量用private,成员函数用public,为了防止用户修改数据嘛

3.成员变量的命名规则

class Date
{
public:
	void Init(int year)
	{
		// 这里的year到底是成员变量,还是函数形参?
		year = year;
	}
	void print()
	{
		cout << year << endl;
	}
private:
	int year;
};

int main()
{
	Date d;
	d.Init(2);
	d.print();
}

在这里插入图片描述
有些时候会出现这种情况,就是不知道year到底是什么,其实是局部变量,因为局部变量优先嘛,所以没有修改对象的year,所以才有了命名规则

class Date
{
public:
	void Init(int year)
	{
		// 这里的year到底是成员变量,还是函数形参?
		_year = year;
	}
	void print()
	{
		cout << _year << endl;
	}
private:
	int _year;
};

int main()
{
	Date d;
	d.Init(2);
	d.print();
}

在这里插入图片描述

4.成员函数的声明与定义的分开

在这里插入图片描述
在这里插入图片描述
如果就是这样写的话,是无法访问到_year的,因为_year相当于在一个作用域里面,所以要写好哪个地方的_year
在这里插入图片描述
这样就可以了

6.类的实例化

类本身不占空间,类实例化出来的对象才占空间
然后就是类的大小的计算,类的大小就是成员变量的大小,而成员函数的话是存放在公共的区域(公共代码区)里的,如果每个对象都存放成员函数,那么浪费的空间太多了
而成员变量的大小就是以前算结构体大小的方法,就是结构体的内存对齐规则

class Date
{
public:
	void Init(int year);
	void print();
private:
	int _year;
	int _month;
	char a;
};

在这里插入图片描述
结构体的内存对齐规则:

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
// 类中仅有成员函数
class A2 {
public:
   void f2() {}
};
// 类中什么都没有---空类
class A3
{};

注意这两个的话的大小是一个字节,理论上应该是没有字节大小的,但是可能吗,你创建一个遍变量还没有大小,这是不可能的,所以会使用一个字节来说明创建了变量,但是不会在里面放数据

7.this指针

我们先定义一个日期类

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 a;
	a.Init(2024, 7, 12);
	a.print();
	return 0;
}

在这里插入图片描述
这里我们提个疑问,为什么调用公共代码区的函数就能改变对应对象的成员变量了呢
原因是你在使用 a.print();的时候,其实是传入了a的地址的了,就是这样的 a.print(&a);
然后对应函数的参数会用一个this指针来接收地址,就是这样


void print(Date*this)
{
	cout << _year << '-' << _month << "-" << _day << endl;
}

然后对应函数的实现就会变成这样

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

但这些步骤都是编译器自己实现的,根本不用你写
而且你写了取地址,和在临时参数那里写了this指针的话还会报错,只有在函数里面写了this才不会有错
在这里插入图片描述
而且this指针是const类型的
this指针是形参所以一般会存在栈区,不会存在对象中,不会占据对象空间,有些时候它还会存在寄存器中

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
 void Print()
 {
 cout << "Print()" << endl;
 }
private:
 int _a;
};
int main()
{
 A* p = nullptr;
 p->Print();
 return 0; }

这个程序会正常运行, p->Print();这个看似好像对空指针进行了解引用,其实嘛,这只是个调用函数而已,调用函数之前要传参,我们说过,这个调用函数其实就是传地址罢了,p->说明是传p的地址,穿的就是nullptr空指针,传空指针肯定不会报错啊,所以没问题

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:
    void PrintA() 
   {
        cout<<_a<<endl;
   }
private:
 int _a;
};
int main()
{
    A* p = nullptr;
    p->PrintA();
    return 0; }

这个就有问题了,因为this指针是空指针,所以cout<<_a<<endl;这一步的实际操作是 cout<_a<<endl;对空指针解引用了,虽然不会报错,因为对空指针解引用是不会报错的,只会运行崩溃

8.构造函数

编译器有六个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
这里我们先讲一个构造函数
构造函数的作用就相当于init的作用
1.构造函数函数名与类名相同
2.它没有返回值,甚至连void都不需要写,如果你写了void就不是构造函数了

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date a;
	a.print();
	return 0;
}

构造函数的使用时间是在你定义对象的时候系统会自动使用了,没有参数的构造函数就是这样使用的,定义的时候就使用了

class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date a(1,2,3);
	a.print();
	return 0;
}

有参数的构造函数是这样使用的
3.构造函数是允许重载的,意思是可以定义多个构造函数

class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
		Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

你怎么使用就决定了使用哪个构造函数
Date a();无参的构造函数不能这样使用,因为这样无法与函数的声明区分开,就只能这样用Date a;

class Date
{
public:
	Date(int year=1,int month=2,int day=3)
	{
		_year = year;
		_month = month;
		_day = day;
	}
		Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date a;
	a.print();
	return 0;
}

这里系统会调用无参的构造函数,就会有歧义,不知道调用哪个
上述我们手动写出来了构造函数,这个构造函数叫做显示定义构造函数
如果我们没有手动生成显示定义构造函数,那么编译器就会自动生成无参的默认构造函数
这个无参的默认构造函数也是在定义变量的时候就会调用的
它是怎么实现的呢

class stack
{
public:
	stack()
	{
		cout << "	stack()" << endl;
	}
private:
	int a;
};

class Date
{
public:
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	stack a;
};

int main()
{
	Date a;
	a.print();
	return 0;
}

在这里插入图片描述
编译器自动生成无参的默认构造函数对于内置类型(基本类型)(也是int char 这种类型)没有处理,有的编译器可能是处理的随机值,有的编译器可能处理的是0,VS处理的就是随机值,而对于自定义类型(也就是结构体 类这种)默认构造函数对于他们的处理就是调用他们的构造函数,如果还没有,就又是系统自动生成的函数就可以了

内置类型成员变量在声明中可以给变量默认值,相当于缺省参数那种用法

class Date
{
public:
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year=16;
	int _month=12;
	int _day=10;
};

int main()
{
	Date a;
	a.print();
	return 0;
}

在这里插入图片描述
如果成员变量内置类型有默认值,那么在定义对象的时候就会最先去走默认值这一步,先去把year month day 给赋值了再说,然后又是因为是默认构造函数,所以year这些变量有了值就是这些值了,不会改变了

class Date
{
public:
	Date()
	{
		_year = 28547;
		_month = 234;
		_day = 24;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year=16;
	int _month=12;
	int _day=10;
};

int main()
{
	Date a;
	a.print();
	return 0;
}

在这里插入图片描述
已经说过了,要先去走默认值,再去走构造函数,所以结果是这样的,这两步都是系统自己干的

无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。这些都是可以不用写参数就能调用的构造函数

一般建议要写构造函数,而且是全缺省的构造函数,还要写内置类型的成员变量的默认值

8.析构函数

所谓析构函数,的作用就相当于destroy的作用而已

1.析构函数的名字是类名前面加上~
2.析构函数也没有返回值,也不需要写void,会在对象的生命周期结束后自动调用,析构函数也没有参数

class stack
{
public:
	stack(int m=4)
	{
		_a = (int*)malloc(sizeof(int) * m);
		_capacity = 0;
		_size = 0;
	}
	void print()
	{
		/
	}
	~stack()
	{
		cout << "~stack()" << endl;
		if (_a)
		{
			free(_a);
			_a = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _a;
	int _capacity;
	int _size;
};

int main()
{
	stack a;
	return 0;
}

在这里插入图片描述
3.注意析构函数只有一个,不能重载
4.如果析构函数我们没有定义,没有显示定义,那么系统会自动生成默认的析构函数
而这个系统会自动生成默认的析构函数和构造函数差不多,对于内置类型不管不处理,对于自定义类型就会调用它的析构函数
所以这样系统默认的析构函数就会有问题,对于指针这个内置类型就不会处理,就不会释放空间
所以析构函数还是要自己写才行

9.拷贝构造函数

所谓拷贝构造函数就是在初始化一个对象的时候,直接复制另一个对象的内容的这个函数
这个还是构造函数,是构造函数的重载形式
拷贝构造函数的形参必须只有一个,而且是另一个对象的引用

class Date
{
public:
	Date()
	{
		_year = 28547;
		_month = 234;
		_day = 24;
	}
	Date(Date& a)
	{
		_year = a._year;
		_month = a._month;
		_day = a._day;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year = 16;
	int _month = 12;
	int _day = 10;
};

int main()
{
	Date a;
	Date b(a);
	b.print();
	return 0;
}
Date b(a);就这样还是在创建变量的时候就调用构造函数,加个(a)就说明用的是拷贝构造
int main()
{
	Date a;
	Date b=a;
	b.print();
	return 0;
}

有些时候也这样写,都一样的,都是拷贝构造,但这个也反映了一个事情,自定义类型的赋值会调用拷贝构造
拷贝构造的形参必须是对象的引用,为什么不能直接写Date a呢
在这之前,我们先弄明白一个事情

class Date
{
public:
	Date()
	{
		_year = 28547;
		_month = 234;
		_day = 24;
	}
	Date(Date& a)
	{
		cout << "Date(Date& a)" << endl;
		_year = a._year;
		_month = a._month;
		_day = a._day;
	}
	void print()
	{
		cout << this->_year << '-' << this->_month << "-" << this->_day << endl;
	}
private:
	int _year = 16;
	int _month = 12;
	int _day = 10;
};

void push(Date b)
{
	cout << "void push(Date b)" << endl;
}

int main()
{
	Date a;
	push(a);
	return 0;
}

在这里插入图片描述
这个例子的结果也说明自定义类型的赋值会调用拷贝构造
回到前面,如果是这样写的拷贝构造

	Date(Date a)
	{
		cout << "Date(Date& a)" << endl;
		_year = a._year;
		_month = a._month;
		_day = a._day;
	}
	Date a;
	Date b(a);

拷贝构造函数在调用之前会先进行参数的赋值,参数的赋值就会调用拷贝构造函数,调用这个拷贝构造函数又会这样,就这样就会一直死递归,所以不行
为什么不取地址来拷贝构造呢,因为就是这样规定的,直接传a就可以了,不传&a

若未显式定义,编译器会生成默认的拷贝构造函数,编译器会生成默认的拷贝构造函数就是直接拷贝那个对象的数据,按字节一个一个拷贝

而这个编译器会生成默认的拷贝构造函数也会出现一些问题,就是在拷贝指针开辟的空间的的时候,就只是单纯的拷贝指针,导致两个对象的指针指向同一块空间,那么这样就会free两次,就会出问题
而正确的操作应该是我们拷贝指针对应的空间,所以开辟空间的拷贝构造函数还是要自己实现的好,如果没有开辟空间,可以自己不实现

然后就是内置类型是按字节拷贝,自定义类型就是按照它的拷贝构造函数去实现的

下面我们来实现一个栈的拷贝构造

class stack
{
public:
	stack(int m = 4)
	{
		_a = (int*)malloc(sizeof(int) * m);
		_capacity = 0;
		_size = 0;
	}
	stack(stack&s)
	{
		_a = (int*)malloc(sizeof(int) * s._capacity);
		memcpy(_a, s._a, sizeof(int) * s._size);
		_capacity = s._capacity;
		_size = s._size;
	}
	void print()
	{
		/
	}
	~stack()
	{
		cout << "~stack()" << endl;
		if (_a)
		{
			free(_a);
			_a = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _a;
	int _capacity;
	int _size;
};

这里我们就可以看出来了,只要有开辟空间的拷贝构造就要自己实现了

总结

这一节都到此结束了,下一篇博客接着写类和对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值