C++ 类和对象

Posted 正义的伙伴啊

tags:

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

对面向对象(OOP)的初步认识

  • C语言是面向过程 的,关注是处理数据的过程,分析出求解问题的步骤,通过函数调用逐步解决问题。数据 和 处理数据的方法是分离的。
  • C++是 基于面向对象 的,关注的是 对象 ,将一件事情拆分成不同的对象,靠对象之间交互完成。而C++ 将数据 和 处理数据的方法封装在一起,包含数据完整的生命周期。 但是C++也不是纯面向对象的语言,C++由于向下兼容C语言使得其也有面向对象的特性。

类的引入

C语言中我们学过 一种自定义类型——结构体 (可以参考博文:自定义类型详解
C语言中结构体是一种数据类型,可以表示不同数据类型的一种集合,在C++中对struct的作用进行了延申,struct 里面不仅可以定义 数据 还可以定义函数!


struct Student
{
	void SetStudentInfo(const char* s, int a)
	{
		strcpy(name, s);
		age = a;
	}
	void print()
	{
		cout << name << "    " << age << endl;
	}
	char name[20];
	int age;
};
int main()
{
	Student s;
	s.SetStudentInfo("Peter", 20);
	s.print();
	return 0;
}

这里就要引出我们在C++中更喜欢用class来代替struct来表示这种新的数据类——

类的定义

由上面的引入 得出了 组成类的成员

  • 类中的数据——成员变量
  • 类中的函数——成员函数

下面是类的定义方式:


class classname
{

	类的体:由成员函数 和 成员变量 组成

};   注意这里的分号

class ——定义类的关键字 ,classnaeme——类的名字,{}里包含的是类的主体。

类的定义方式:

  • 1.(函数)声明和定义放在一起(这里的定义指的是函数,成员变量在类中只是声明!)
class Student
{
	void SetStudentInfo(const char* s, int a)
	{
		strcpy(name, s);
		age = a;

	}
	void print()
	{
		cout << name << "    " << age << endl;
	}
	char name[20];
	int age;
};

注意: 这里在类里面定义函数 编译器一般会把其当成内联函数处理,所以小一点的函数可以在内里面定义,但是大一点的函数定义和声明还是分离比较好

  • 2.(函数)声明和定义分开

可以声明在头文件 Student.h中


class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();
	char name[20];
	int age;
};

定义放在 Student.cpp中,使用:: 运算符

#include<Student.h>
void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;

}
void Student::print()
{
	cout << name << "    " << age << endl;
}

类的访问限定符及封装

访问限定符


说明

  1. public修饰的成员可以被类外直接访问
  2. proteced和private修饰的成员在在类外不可以直接被访问
  3. 访问权限的作用域 是从该访问限定符出现的位置到下一个访问出现的位置之前
  4. class的默认访问权限 为private,struct为public(因为要兼容C,这也是class和struct在表示类时的唯一区别)

问题:class和struct 有什么区别?
在C语言中struct可以表示成结构体去使用。C++由于兼容了C语言的特性,所以struct既能表示结构体,又能表示类并且和class作用一样,唯一不同的是:class的默认访问权限 为private,struct为public

封装

首先我们要了解一下面向对象的三大特性:封装、继承、多态
什么是封装呢?
封装是将数据和操作数据的方法进行有机的结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
说大白话:就是想让你访问的就是公有,不想让你访问的就设成私有,你必须通过成员函数才能与数据交互

总结
封装实际上是一种对数据的管理,防止乱访问数据造成的修改

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用:: 作用域解析符指明成员属于哪个类

class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();
	char name[20];
	int age;
};
//这里要指明setStudentinfo是来自Student这个类域
void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;
}

void Student::print()
{
	cout << name << "    " << age << endl;
}

类的实例化

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

  1. 类只是一个模型,和struct表示的结构体一样是一个类型集合,定义一个类并没有实际给其分配内存空间来储存
  2. 一个类可以实例化多个对象,对象是类似于定义的变量,占有内存

打个比方:一个类定义出来就类似于一个图纸,类实例化出的对象就类似于按照图纸造出的房子,有了图纸你就可以造出房子,但图纸并没有实际存在的房子


class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();


	char name[20];   //变量声明 不开辟空间
	int age;
};

void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;

}
void Student::print()
{
	cout << name << "    " << age << endl;
}


int main()
{
	Student s;   // 实例化的对象
	s.SetStudentInfo("Peter", 20);
	s.print();
	return 0;

}

类的大小的计算

如何计算一个类的大小呢?

  1. 解决这个问题首先要知道如何处理类中成员函数所占的空间
    这里成员函数其实是不存储在类里的,而是存储在内存分区中的 代码区。代码区存的都是在编译后 代码转换成的指令。而类的实例化是在堆栈上开辟的空间,所以在计算内存中无需考虑成员函数的大小,只 需要考虑成员变量。
  2. 然后类的大小分配原则和结构体的一模一样——都是按照内存对齐
    具体 可以参考博客:结构体的内存对齐规则

class Student
{
public:
	void SetStudentInfo(const char* s, int a);
	void print();
	char name[20];
	int age;
};

void Student::SetStudentInfo(const char* s, int a)
{
	strcpy(name, s);
	age = a;

}
void Student::print()
{
	cout << name << "    " << age << endl;
}


int main()
{
	cout << sizeof(Student) << endl;
}

由内存对齐规则可以知道结果是24

特殊的类的大小

class A
{
public:
	void f2();
};


class B
{};

int main()
{

	cout << sizeof(Student) << endl;
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
}

这里两个类 A 和 B并没有成员变量,按照上面的计算 内存应该为0,但是这里 人为的规定其大小为1 ,这里给一个字节是为了占位,表示对象存在,但是不存储任何有效数据!

类成员的this指针

我们先定义一个Data类:


class Data
{
public:
	void print()
	{
		cout << "year: " << _year << " month: " << _month << " day: " << _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()
{
	Data d1;
	Data d2;
	d1.SetDate(2021, 10, 12);
	d2.SetDate(2020, 10, 12);
}

对于上述类,有一个问题:
Data类中由SetData与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetData函数时,该函数是如何区分应该设置d1对象还是设置d2对象呢?

C++中通过引入this指针来解决这个问题,C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作都是自动、隐式的,不用用户主动去调用。

this指针的特性

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

所以这里d1.SetDate(2021, 10, 12); 这句函数调用也就相当于 SetDate(&d1,2021,10,12);

注意:

  1. this指针存储在哪里?
    this指针不是存储在对象里面!!this指针是形参,形参和函数中的局部变量都是存储在函数栈帧里面的,实际上是由ecx寄存器传入
  2. this指针不能为空

下面这段代码能让我们更深刻的了解 成员函数 和 this指针


class A
{
public:
	void printA()
	{
		cout << _a << endl;
	}

	void show()
	{
		cout << "show()" << endl;
	}
private:
	int _a;
};


int main()
{
	A* p = nullptr;
	p->printA();  //语句1
	p->show();    //语句2
}

问题:

  1. 这段代码能通过编译吗?能正常运行吗?
  2. 单独运行语句2 能正常运行吗?

首先这段代码是可以通过编译的,但是会在运行阶段挂掉,且中断在printA函数的_a调用上。

一部分同学会认为编译无法通过的原因是 :p是一个空指针,对空指针解引用调用函数不是瞎搞吗?所以这里无法通过编译。这是对成员函数的存储位置还不是很了解,前面讲过成员函数是不储存在对象里面的,而是储存在内存上的代码区上,这里调用成员函数不回去访问p指向的空间,也就不存在对空指针解引用。
实际上:这里进行的操作是把 p(NULL)的值传给this指针!!

后面的现象也就很好解释了,传给this指针 show函数并没有调用类里面的变量,而printA函数调用了变量_a,而我们知道实际上这里调用的是this->_a,所以这里才出现了对空指针的解引用。

类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写任何东西的情况下都会默认生成 ——6个默认成员函数


注意:

  1. 默认成员函数在类定义之时,就会生成,但是如果自己定义了就不会再生成
  2. 默认成员函数也不是必须自己写,当默认成员函数能完成功能就不用自己写,如果不能完成功能 例如下面会讲到的 stack类,构造、析构、拷贝构造、赋值重载都要自己写!

构造函数

class Data
{
public:
	void print()
	{
		cout << "year: " << _year << " month: " << _month << " day: " << _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()
{
	Data d1;
	Data d2;
	d1.SetDate(2021, 10, 12);
	d2.SetDate(2020, 10, 12);
}

前面在实现日期类的时候,写过一个函数是void SetDate(int year, int month, int day) 这个函数的主要目的是对成员变量进行初始化,这里其实的功能和构造函数一模一样,都是对类成员变量进行初始化,但是缺点也是很明显定义与初始化是分离的,每次定义完要调用函数初始化。构造函数直接在定义的时候就可以初始化了。

  • 构造函数——名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。

  • 默认构造函数——不用传参就可以调用的构造函数

特性

注意构造函数是初始化对象,而不是定义构造函数(开辟空间)
其特征如下:

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载——提供多种初始化对象的方式。

class Data
{
public:
	Data()  //无参的构造函数
	{
		;
	}

	Data(int year , int month , int day ) //带参数的构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Data d1;       
	Data d2(2021,7,26);
	
	Data d3(); //注意无参构造函数初始化对象时,对象后面不用跟括号,否则就成函数声明了
	//这里就是一个无参返回值是类Data的函数名为d3的函数
}


  1. 如果没有显示的定义构造函数,则C++编译器会自动生成一个无参默认构造函数,一旦用户显示定义编译器就不会再生成
class Data  //这里没有定义构造函数,编译器就会自己生成一个
{
private:
	int _year;
	int _month;
	int _day;
};

那么编译器生成的构造函数能完成什么功能呢?

  • 对待内置类型例如:int ,double ,指针,long…默认构造函数是不会初始化的
  • 对待自定义类型(class、struct)会调用它的 默认 构造函数(不用传参数的构造函数)

class A
{
public:
	A(int a = 100)
	{
		_a = a;
	}
	int _a;
};


class Data
{
	void print()
	{
		cout << ps._a <<endl<<_year<<endl<<_month<<endl<<_day<< endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A ps;
};

int main()
{
	Data d1;
	d1.print();
}

这里在类Data成员变量中定义了 三个int类型和一个类A类型的变量,最后只有自定义类型的变量被初始化了


  1. 无参构造函数和全缺省构造函数 都称为默认构造函数,并且默认构造函数只能有一个! 注意:无参构造函数全缺省构造函数编译器自己生成的构造函数 统称为 默认构造函数

误区: 只有编译器自己生成的构造函数才是默认构造函数


class Data
{
public:
	Data()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}

	Data(int year=0 , int month=1 , int day=1 ) //两个默认构造函数不能同时存在
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

不能存在两个默认构造函数,如果在类的实例化时不传参,编译器不知道调用哪一个构造函数。

总结:
大多数情况下构造函数都要自己去写,因为初始化出来的变量才会符合要求。一般情况下建议写一个全缺省的构造函数,这样可以应对各种场景。

参数列表

上面我们介绍了构造函数,其中有一种特殊的构造函数:默认构造函数。默认构造函数对待内置类型是不处理的,对待自定义类型是调用自定义类型的 默认构造函数 (注意是 默认构造函!!!!)
但是这里遗留了一个问题,如何对自定义类型里面的变量赋值?

class B
{
public:
	B(int x=1,int y=2)
	{
		_x = x;
		_y = y;

	}

	const B& operator=(const B& d1)
	{
		_x = d1._x;
		_y = d1._y;

		return *this;
	}
private:
	int _x;
	int _y;

};

class A
{
public:
	A(int a,int b,int c)
	{
		B b2(b,c);
		b1 = b2;
		// b1= B(b,c)  使用匿名对象:生命周期只有这一行!
		_a = a;
	}
	
private:
	int _a;
	B b1;
};

int main()
{
	A a1(100,100,100);
}

如果想要把值赋给类A中的类B成员变量,只能先创建一个临时变量赋值,然后再用赋值运算符重载拷贝给成员变量。这样定义类中的 类对象会十分麻烦。

这里我们就要介绍一下初始化列表:
初始化列表: 以一个冒号开始,接着是以一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式

	A(int a,int b,int c)         //未显示定义参数列表,但是参数列表依然存在                            
	{
		B b2(b,c);
		b1 = b2;
		_a = a;
	}
	
    A(int a,int b,int c)//使用初始化列表
		:_a(a)
		,b1(b,c)
	{}

之所以参数列表能解决上面的问题,实际上是自定义类型会在参数列表处初始化(而自定义的 初始化 和 变量赋值是绑定在一起的),如果我们能在初始化的时候赋值就可以调用非默认构造函数了,而不是通过创建临时变量赋值重载。

参数列表不管你是否显示的写出来一直是存在的,而且一切变量都会在参数列表处初始化(如果我们对未显示定义参数列表的构造函数按f11一步一步的调试,会发现实际上在进入构造函数之前会跳到类B的默认构造函数)

注意:

  • 每个成员只能在初始化列表上出现一次

  • 类中包含以下成员必须在初始化列表处初始化:

    1. 引用成员变量
    2. const成员变量
    3. 自定义类型成员变量(该类没有默认构造函数)

可以发现必须在初始化列表初始化的成员变量有一个共性:定义的时候就必须赋初值,赋初值是和定义是绑定在一起的。

易错点



class A
{
public:

	A(int x)
		:_a1(x)
		, _a2(_a1)
	{}
	
	void print()
	{
		cout << _a1 << "    " << _a2 << endl;
	}

private:
	int _a2;
	int _a1;
};

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

结果:很多人会认为结果是 1 1,但实际上是:

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

析构函数

概念:
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时

以上是关于C++ 类和对象的主要内容,如果未能解决你的问题,请参考以下文章

C++类和对象--继承

C++类和对象1

成功创建c ++类和对象[重复]

C++从青铜到王者第二篇:C++类和对象(上篇)

C++类和对象的简单应用举例

C++初阶类和对象