C++初阶---类和对象

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶---类和对象相关的知识,希望对你有一定的参考价值。

1)面向过程与面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题


C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成
注意: C++是基于面向对象语言,不像Java等是面向对象语言


2)类

①类(class)的引入和定义

引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数,已经升级为类

例如:

struct Student
{
	void SetStudentInfo(const char* name, 
						const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void PrintStudentInfo()
	{
		cout<<_name<<" "<<_gender<<" "<<_age<<endl;
	}
	char _name[20];
	char _gender[3];
	int _age;
};

主函数中调用即可

Student s;
s.SetStudentInfo("Jack", "male", 38);

但在C++中更喜欢用class关键字来代替struct


C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是
private


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

类中的元素称为类的成员,类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数

类的两种定义方式

  1. 声明和定义都在类体中
class person
{
public:
	void test()
	{
		//code...
	}
private:
	char _a;
	char _b;
	int _c;
};
  1. 声明在类体中,定义放在类实现文件中
class person
{
public:
	void test();
private:
	char _a;
	char _b;
	int _c;
};
//分开
void person::test()
{
	//code...
}

注意

  1. 成员函数如果在类中定义,编译器可能会将其当成内联函数处理
    (小于10行)
    参考C++基础入门概览的inline函数部分
  2. 一般情况下,更期望采用第二种方式

②类的访问

1. (初识)类的访问限定符
  1. public
    修饰的成员在类外可以直接被访问
  2. private
    修饰的成员在类外不能直接被访问
  3. protected
    此处暂时认为protected和private是类似的

注意

  1. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  2. 在类的成员全部都是pulic的时候,我们可以使用struct来定义类
  3. 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

2. 类的作用域

如上面定义类的第二种方式所示,在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域

void person::test()
{
	//code...
}

3. 面向对象的三大特性之一:封装

面向对象的三大特性:封装,继承,多态
注意:面向对象的特性不止这三个


封装的定义:

将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
(类的定义就体现了封装)

封装的好处:

我们使用类数据和方法都封装一下。不想让别人看到的,我们使用protected/private把成员封装起来,开放一些共有的成员函数对成员合理的访问,封装本质是一种管理
不封装会要求使用的人必须非常规范


③类的实例化和对象模型

实例化

用类创建对象的过程,称为类的实例化
注意

  1. 类只是一个图纸一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
1. 计算类对象的大小

例1

class A1
{
public:
	void PrintA()
	{
		cout<<_a<<endl;
	}
private:
	int _s
	char _a;
};

例2:

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

例3:

// 类中什么都没有---空类
class A3
{};

sizeof(A1) sizeof(A2) sizeof(A3) 分别为8 , 1 , 1
请看类对象的存储方式

2. 类对象的存储方式
  1. 对象中包含类的各个成员

    缺陷每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间,所以我们采用第二种存储方式

  1. 只保存成员变量,成员函数存放在公共的代码段

总结
一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类(此1字节不存储有效数据)

3. 结构体内存对齐规则

与C语言中的结构体内存对齐规则完全一致
参考C语言----结构体,枚举,共用体


3)this指针

概念

例子

class Date
{
public :
	void Display ()
	{
		cout <<_year<< "-" <<_month << "-"<< _day <<endl;
	}
	void SetDate(int year , int month , int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private :
	int _year ; // 年
	int _month ; // 月
	int _day ; // 日
};
int main()
{
	Date d1, d2;
	d1.SetDate(2018,5,1);
	d2.SetDate(2018,7,1);
	d1.Display();
	d2.Display();
	return 0;
}

打印 2018-5-12018-7-1


创建了两个实例化对象d1,d2,调用函数时是如何知道是d1还是d2呢?


this指针C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成
特性

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

所以

void Display()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

其实是:

void Display(Data* this)
{
	cout << this->_year << "-" << this->_month << "-" << this->_day << >endl;
}

this指针的一些问题

1.this指针存在哪

this指针是形参,形参和函数中的局部变量都是存在函数栈帧里,所以this指针可以认为存在栈里

2.this指针可否为空指针

例子:

class A
{
public:
	void PrintA()
	{
		cout<<_a<<endl;
	}
	void Show()
	{
		cout<<"Show()"<<endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->PrintA();
	p->Show();
}

调用PrintA()函数直接崩溃,而调用Show()函数正常运行


分析:

  1. 成员函数地址不在对象中存储,在公共代码段,这里调用成员函数Show(),不会访问p指向的空间,也就不存在空指针解引用了,这里智慧把p传递给隐含this指针。但是Show()函数中也没有解引用this指针
  2. 同理,PrintA()函数解引用了空指针,所以崩溃

4)类的默认成员函数

概览

任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数


类的6个默认成员函数

  1. 构造函数完成初始化工作
  2. 析构函数完成清理工作
  3. 拷贝构造函数使用同类对象初始化创建对象
  4. 赋值操作符重载把一个对象赋给另一个对象
  5. 取地址操作符重载:???
  6. const取地址操作符重载:???

①构造函数

概览

如下代码所示

class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1,d2;
	d1.SetDate(2018,5,1);
	Date d2;
	d2.SetDate(2018,7,1);
	return 0;
}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,过于麻烦
为此引出构造函数:


定义
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器 自动调用保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
注意
构造函数是特殊的成员函数,其主要任务并不是开空间创建对象,而是初始化对象


特性

注意默认构造函数(不传参就可以调用的那个函数)


特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载(见下方分析)
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成(见下方分析)
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数(见下方分析)
  7. 默认构造函数对于内置类型调用编译器自己生成的默认构造函数貌似没有用但是对于自定义类型就有用了
  8. 成员变量的命名风格(见下方分析)

特性分析:
特性4

构造函数重载

class Date
public :
// 1.无参构造函数
	Date ()
	{}
	// 2.带参构造函数
	Date (int year, int month , int day )
	{
	_year = year ;
	_month = month ;
	_day = day ;
	}
private :
	int _year ;
	int _month ;
	int _day ;
};
int main(){
	Date d1; // 调用无参构造函数
	Date d2 (2015, 1, 1); // 调用带参的构造函数
}

此处注意
只有有参的构造函数时不能无参调用,编译器会报错


特性5

类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(不传参就可以调用的那个函数),一旦用户显式定义编译器将不再生成

class A
{
public:
	A()
	{
	_a1=0;
	_a2=1;
	}
private:
	int _a1;
	int _a2;
};
class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	Date (int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	*/
private:
	int _year;
	int _month;
	int _day;
	A _a;
};
int main()
{
	Date d;// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
}

但是我们可以看到,创建的对象只要没有定义了的构造函数都是随机值

解释
编译器默认生成构造函数的时候

  1. 内置类型不会初始化(内置类型是指 int,char…)
  2. 自定义类型才会调用他的无参构造函数进行初始化
  3. 如果类A中也没有自定义构造函数,同样也会是随机值
  4. 注意 默认生产的构造函数发现类中没有无参构造函数时也会报错(解释的不清)

在c++11中,语法委员会打了一个补丁,如下可以用缺省值进行初始化

class Date
{
pubic:
	Date (int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	
private:
	int _year=1;
	int _month=1;
	int _day=1;
	A _a;
};

注意此处仍是成员变量定义,像函数给缺省值那样,不是初始化


特性6

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个


下面代码会出错

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

解释
编译器会出现歧义,不能明确知道调谁
注意
无参构造函数全缺省构造函数我们没写编译器默认生成的构造函数,都可以认为是默认成员函数


特性8

如下代码

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

解释
创建出来的对象中的year是随机值,局部性原则,因为year = year;这条语句以最近的year为主
如果想要正确初始化应该是this->year=year;


所以我们给类成员变量命名时建议在前面加一个标识符‘_’,如_year

②析构函数

概览

我们知道一个对象是由构造函数进行初始化的,那么析构函数就是清理空间
定义
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数是特殊的成员函数


特性

特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  5. 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数
特性分析:
特性5

如下代码

class Stack
{
public:
	Stack (int capacity = 10)
	{
		_a = (int*)malloc(capacity * sizeof(int));
		_capacity = capacity;
	}
	~Stack()
	{
	free(_a);
	_a=nullptr;
	_top=_capacity=0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class SeqList
{
public :
	SeqList (int capacity = 10)
	{
		_pData = (int*)malloc(capacity * sizeof(int));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	/*~SeqList()
	{
		if (_pData)
		{
			free(_pData ); // 释放堆上的空间
			_pData = NULL; // 将指针置为空
			_capacity = 0;
			_size = 0;
		}
	}*/
private :
	int* _pData ;
	size_t _size;
	size_t _capacity;
	Stack st;
};



可以看到编译器自己生成的默认析构函数同构造函数一样,不会处理内置类型成员,自定义类型成员会去调用他的析构函数

③拷贝函数构造

概览

在创建对象时,可以创建一个与一个对象一某一样的新对象
定义
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

特性

特性

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
  4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们仍需要自己实现,像Stack这种类,不能使用字节序拷贝
特性分析
特性2

拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
(编译器会强制检查终止程序)


假设我们的拷贝构造函数是这样的:
(Date d=d2也是拷贝构造语法)

Date(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
int main()
{
	Date d2(2020,1,1);
	Date d3(d2);
	//也可以Date d3=d2;
}

生成默认拷贝构造函数无穷递归


所以我们使用引用传参
同时为了防止出现d._day = _day;的情况,不是输出型函数用const进行保护

Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	//d._day = _day;
}
int main()
{
	Date d2(2020,1,1);
	Date d3(d2);
	//也可以Date d3=d2;
}

特性3 和 特性4

在编译器生成默认拷贝构造函数时和之前的构造函数·析构函数不同
拷贝构造函数不会区分内置类型和自定义类型成员

  1. 对于内置类型成员拷贝构造函数对象按内存存储按字节序完成拷贝,为浅拷贝或值拷贝
  2. 对于自定义类型,会去调用他的拷贝构造函数进行构造

如下代码:
对于Stack这种类必须自己写拷贝构造函数

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{// 像Stack这样的类,对象中的资源需要清理工作,就用析构函数
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int mainC++初阶第四篇——类和对象(上)(类的定义+封装+this指针)

C++初阶类和对象

C++ 初阶类和对象

C++ 初阶类和对象

C++初阶 —— 类和对象(上篇)

C++初阶 —— 类和对象(下篇)