到底什么是类?什么是对象?
类是一个抽象的概念,它不存在于现实中的时间/空间里,类只是为所有的对象定义了抽象的属性与行为。
类是一个静态的概念,类本身不携带任何数据。当没有为类创建任何对象时,类本身不存在于内存空间中。 对象是一个动态的概念,每一个对象都存在着有别于其它对象的属于自己的独特的属性和行为,对象的属性可以随着它自己的行为而发生改变。
就好比:你是人,人是一个概括,是一个类,是虚拟不存在的;而你是真实存在的,有血有肉,你就是“人”这个类中具体的对象。
封装:
所谓封装就是将某些东西包装盒隐藏起来,让外界无法直接使用,只能通过某些特定的方式才能访问。封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是通过外部接口以及特定的访问权限来使用类的成员。
我们可以通过封装使一部分成员充当类与外部的接口,而将其它的成员隐藏起来,这样就限制了外部对成员的访问,也使不同类之间的相互影响度降低。
访问限定符:
类由两部分构成,类声明和类体:
class 类名
{
类体的内容
}
class是关键字,用来定义类,“class类名”是类的声明部分,两个大括号以及中间的内容是类体,类体中包括了数据部分和对这些数据进行操作的函数,而这就体现了把数据和操作封装在一起。封装在类对象中的成员都对外界隐蔽,不能调用。所以需要用到“访问限定符” :
public:公有成员,既能被本类内的成员函数所引用,也可以被类的作用域内的其他函数引用,也就是任何位置都可以访问
private:私有成员,不能被外部访问,本类中访问
protected:声明的成员称为受保护的成员,子类,只能在本类中允许访问
类中实现成员方法默认时inline
在面向对象的程序设计中,在声明类时,一般都是把所有的数据指定为私有的,使它们与外界隔离。把需要让外界调用的成员函数设置为公用的。在类外虽然不能直接访问私有成员,但是可以通过调用公用成员函数来引用甚至修改私有数据成员。
例如,这样一个类,我们把数据和功能封装起来,并展示一下访问限定符的作用:
class Animal
{
public: //这就是公共成员,外部的接口
void SetAnimalName(string strname);
void ShowAnimalName();
private: //这是私有成员,外部是无法访问到的
string m_strName;
};
对象的生成与销毁:
对象的生成:(1)对象开辟内存空间
(2)调用构造函数对内存空间进行初始化(赋资源)
对象的销毁:(1)释放对象所占的其他资源(调用析构函数)
(2)释放对象所占的内存空间
什么是this指针:指向对象内存的地址
在每个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,就是this指针,它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。例如,当调用成员函数a.volume时,系统就把对象a的起始地址赋给this指针,于是在成员函数引用数据成员时,就按照this指针所指的地址找到对象a的数据成员。普通的成员方法是thiscall的调用约定,普通的成员方法依靠对象调用。
什么是重载函数:
c++中在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形参(参数的个数,类型或者顺序)必须不停,也就是说同一个运算符完成不同的运算符。常用来实现功能类似而所处理不同的数据不同的问题,重载函数的返回值必须相同。
浅拷贝和深拷贝的区别:
浅拷贝:没有创建内存,只赋值地址。
深拷贝:创建了内存,把值全部拷贝一份。
区别在于: 复制时,指针是否有重新开辟内存空间。
c++中为了方便对象的使用,定义了六个默认函数(若程序不定义或不调用,编译器自动定义或调用的函数)
一、构造函数:
构造函数是一种特殊的成员函数,与其他成员函数不同,它不需要用户来调用它,而是在建立对象时自动执行。构造函数是在声明类的时候由类的设计者定义的,程序用户只需在定义对象的同时指定数据成员的初值即可。构造函数的名字必须与类名相同,以便编译系统能识别为构造函数来处理。它不具有任何类型,不返回任何值。是用来初始化对象的内存空间。
特点:可以重载 、函数名就是类名、有this指针、按照定义顺序构造
不能手动调用 :因为是调用构造函数来对对象的开辟的内存空间进行初始化,而对象的生成又靠构造函数生成,如果此时是手动调用,没有对象生成的内存空间,如何初始化?举个例子,人不能选择自己的出生。
class Student
{
public:
Student(char* name, bool sex, int age)//完全构造
{
std::cout << this <<" :Student::Student(char*,bool,int)" << std::endl;
mname = new char[strlen(name) + 1]();
strcpy(mname, name);
msex = sex;
mage = age;
}
Student(char* name, bool sex)//不完全构造
{
std::cout << this << " :Student::Student(char*, bool)" << std::endl;
mname = new char[strlen(name) + 1]();
strcpy(mname, name);
msex = sex;
}
Student()//系统默认的构造函数(空函数)
{
std::cout << this << " :Student::Student()" << std::endl;
}
}
二、析构函数:用来销毁对象开辟的内存空间
作用:1.防止内存泄漏、2.释放对象所占的其他资源。
特点:不可重载,只有一个 ,先构造再析构(在栈上,符合栈的原理)
1 .可以手动调用 : 本来默认是当清栈(是在栈上开辟)时,系统会调用这个函数来释放对象的内存空间,但是手动来调用去释放也是可以的。举个例子,人可以活到寿命终了后去世,也可以自己手动killmyself。
2. 一个事例: str1.~student(); 这种情况下,会退化成普通的成员方法,不会影响到系统调用析构函数去销毁资源,会导致内存块被释放两次,系统崩溃。
~Student()
{
std::cout << this << " :Student::~Student()" << std::endl;
delete[] mname;
mname = NULL;
}
三、 拷贝构造函数 :拿一个已经存在的对象来生成一个相同类型的新对象。
例如: student st2=st1; st1是一个已经存在的对象,st2是想要生成的一个新的对象。
特点:函数名为类名,只有一个形参(未初始化的对象的引用),形参时类对象的引用,防止递归构造形参对象。
调用拷贝构造函数的情况:
(1) 一个对象作为函数参数,以值传递的方式传入函数体;
(2) 一个对象作为函数返回值,以值传递的方式从函数返回;
(3)一个对象用于给另外一个对象进行初始化(常称为赋值初始化)
如果涉及到指针的话,这个函数系统默认为浅拷贝,但是实际上应该用深拷贝才能成功,结合下图理解:
Student(const Student& rhs) //拷贝构造函数
{
std::cout << this << " :Student::Student(const Student&)" << std::endl;
mname = new char[strlen(rhs.mname) + 1]();
strcpy(mname, rhs.mname);
msex = rhs.msex;
mage = rhs.mage;
}
五、赋值运符的重载函数(operator=): 拿一个已存在的对象赋值给相同类型的已存在对象(默认 浅拷贝)
首先为什么要对赋值运算符“=”进行重载。某些情况下,当我们编写一个类的时候,,并不需要为该类重载“=”运算符,因为编译系统为每个类提供了默认的赋值运算符“=”,使用这个默认的赋值运算符操作类对象时,该运算符会把这个类的所有数据成员都进行一次赋值操作。
例如: good1=good2;
通过使用系统默认的赋值运算符“=”,可以让对象good2中的所有数据成员的值与对象good1相同。这种情况下,编译系统提供的默认赋值运算符可以正常使用。但是当good1和good2进行析构的时候,由于重复释放了一块内存,导致程序崩溃报错。在这种情况下,就需要我们重载赋值运算符“=”了。
所以这样:
good1.operator=(good2);//进行赋值
当有连等的情况下,比如:
good1=good2=good3; 则必须得有返回值。
四个步骤: 1.判断自赋值
2.释放旧资源(因为默认是浅拷贝,如果不释放旧资源的话,就会造成内寸泄漏,原理如下图)
3.开辟新资源
4.赋值
形参const作用:
1.防止实参被修改
2.接收隐式生成的临时量
class Test
{
public:
Test(int a, int b)
{
std::cout << this << "Test:: Test(int,int)" << std::endl;
ma = a;
mb = b;
}
Test(int a)
{
std::cout << this << "Test:: Test(int)" << std::endl;
ma = a;
}
Test()
{
std::cout << this << "Test:: Test()" << std::endl;
}
Test(const Test& rhs)
{
std::cout << this << "Test:: Test(const Test&)" << std::endl;
ma = rhs.ma;
mb = rhs.mb;
}
Test& operator=(const Test& rhs)
{
std::cout << this << "Test::operator = (const Test&)" << std::endl;
if (this != &rhs)
{
ma = rhs.ma;
mb = rhs.mb;
}
return *this;
}
~Test()
{
std::cout << this << "Test::~Test()" << std::endl;
}
int getValue()
{
return ma;
}
private:
int ma;
int mb;
};
其他知识点:
临时对象
1.隐式生成临时对象 编译器推演需要的对象类型
例如 : Test test7 = 10; 首先利用有整数形参的构造函数,生成一个临时对象,来生成test7这个新对象。但是10并没有定义类型,需要编译器自我推演。
2.显式生成临时对象 程序指明要生成的对象类型
例如: Test test8 = Test(10); 这个地方原理同上,但是10已经定义了类型为 Test,所以是显式生成。
生存周期为:遇到表达式结束。
优化
如果临时对象生成的目的是为了生成新对象,则以生成临时对象的方式来生成新对象,这样就减少了一次生成临时对象的过程,更加优化。
例如:Test test7 = 10;
此处要生成新对象test7,原本应该等号左边生成一个新对象,等号右边生成一个临时对象,然后用临时对象来赋值给新对象;但是如果进行优化,因为要生成新对象,所以就以生成临时对象的方式生成新对象,也就是说 等号右边生成一个临时对象,然后将这个临时对象直接更名为新对象test7,不用再进行赋值,减少一次调用赋值运算符的重载函数的过程,提高效率。
引用:(&和* 都不生成新的对象)
如果有引用则会提升临时对象的生存周期 ,使临时对象变得和引用对象一样的生存周期
例如:Test&rest10=Test(10,20);
原本应该是 调用三个函数,首先Test(10,20)寻到有定义为整形形参的构造函数构造两个对象,然后调用赋值函数,最后再调用析构函数释放开辟的内存。 但是用 &后,提货test10的生存周期,就不用再遇到表达式;时就调用析构函数去释放了,所以就只用调用两个函数就可以了,没有析构函数。
临时量
内置类型 ==》 常量
自定义类型 ==》 变量 内存
隐式生成 ==》 常量
类类型的返回值
都是由临时对象带出来。
其他两个函数不常用到,了解就好。