类和对象万字总结
Posted 一个山里的少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类和对象万字总结相关的知识,希望对你有一定的参考价值。
目录
1..类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
class Date ;
上面这个就是一个空类
2.构造函数
1.对于以下的日期类:
#include<iostream> using namespace std; class Date public: void SetDate(int year, int month, int day) _year = year; _month = month; _day = day; void Display() cout << _year << "-" << _month << "-" << _day << endl; private: int _year; int _month; int _day; ; int main() Date d1, d2; d1.SetDate(2018, 5, 1); d1.Display(); d2.SetDate(2018, 7, 1); d2.Display(); return 0;
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
2.构造函数的特性:
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
#include<iostream> using namespace std; class Date public: // 1.无参构造函数 Date () // 2.带参构造函数 Date (int year, int month , int day ) _year = year ; _month = month ; _day = day ; private : int _year=0 ; int _month=0 ; int _day=0; ; void TestDate() Date d1; // 调用无参构造函数 Date d2 (2015, 1, 1); // 调用带参的构造函数 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 Date d3(); int main() TestDate(); return 0;
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
#include<iostream> using namespace std; class Date public: // 如果用户显式定义了构造函数,编译器将不再生成 /*Date(int year, int month, int day) _year = year; _month = month; _day = day; */ private: int _year; int _month; int _day; ; void Test() Date d;// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数 int main() Test(); return 0;
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
#include<iostream> using namespace std; // 默认构造函数 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 ; ; // 以下测试函数能通过编译吗? void Test() Date d1; int main() Test();
显然是不能够通过编译的
7.默认构造函数的调用时机:
当不使用任何初始值定义一个类的非静态变量时,会调用该类的默认构造函数
A a;
此时,会调用类A的默认构造函数如果类中没有显式地定义默认构造函数,则C++编译器会为其创造一个合成的默认构造函数,如果类中已经定义了其他格式的构造函数,此时C++编译器不会再为其合成默认构造函数。所以,如果此时类A的定义为
class A public: A(int i) ;
此时,程序会报错,报错信息为“error C2512: “A”: 没有合适的默认构造函数可用”这与上面那个例子是相同的
当类B含有类A的对象,并且使用类B的默认构造函数时,会调用类A的默认构造函数
class A public: ; class B A m_a; ;
类本身含有类的对象且没有在构造函数中显式初始化该对象时
class A public: ; class B public: B(int i) A m_a; ;
8.有参构造函数的三种调用方法
#include<iostream> using namespace std; class Test public: //带参数的构造函数 Test(int a) cout << "Test(a)" << endl; Test(int a, int b) cout << "Test(a,b)" << endl; private: int a; int b; ; int main() //1. 括号法:C++编译器调用有参构造函数 Test t1(10); //2. 等号法:C++编译器调用有参构造函数 Test t2 = (20, 10); //3. 构造函数法:手动直接调用构造函数//Test(30)这是一个匿名对象 Test t3 = Test(30); return 0;
9.成员变量的命名风格
一般我们会在成员变量的前面加一个_ 我们来看一下下面这个代码:
// 我们看看这个函数,是不是很僵硬? class Datepublic: Date(int year) // 这里的year到底是成员变量,还是函数形参? year = year; //虽然此处我们可以用this指针来区别但还是建议使用下面这种方式 private: int year; ; class Date public: Date(int year) _year = year; private: int _year; ; // 或者这样。 class Date public: Date(int year) m_year = year; private: int m_year; ;
3.析构函数
1.前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
2.析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。
如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。如果定义了析构函数,则编译器不生成缺省析构函数。
2.析构函数的特性:
析构函数是特殊的成员函数
特性如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
5.如果显示定义了析构函数那么编译器将不在生成
如果我们需要在构造函数里面干一些其他的事情则我们需要手动的写析构函数
#include<stdlib.h> #include<assert.h> #include<iostream> typedef int DataType; class SeqList public: SeqList(int capacity = 10) _pData = (DataType*)malloc(capacity * sizeof(DataType)); 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; ;
3.在创建一类对象数组时,对于每一个数组中的元素都会执行缺省的构造函数,同样对象声生命周期结束时数组中的每个元素的析构函数都会被调用
#include<iostream> using namespace std; unsigned int count1 = 0; class A public: A ( ) i = ++count1; cout << "Creating A " << i <<endl; ~A ( ) cout << "A Destructor called " << i <<endl; private : int i; ; int main( ) A arr[3]; // 对象数组 return 0;
运行结果:
为什么析构函数的调用和构造函数相反了这是因为栈的性质:先进后出
4.析构函数的调用时机:
如果出现以下几种情况,程序就会执行析构函数:
(1)如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
(2)static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
(3)如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。
(4)如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
(5)调用复制构造函数后。
#include <iostream> using namespace std; class CMyclass public: ~CMyclass() cout << "destructor" << endl; ; CMyclass obj; CMyclass fun(CMyclass sobj) return sobj; //函数调用返回时生成临时对象返回 int main() obj = fun(obj); //函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用
运行结果:
destructor // 形参和实参结合,会调用复制构造函数,临时对象析构
destructor // return sobj函数调用返回,会调用复制构造函数,临时对象析构
destructor // obj对象析构
构造函数和析构函数总结:
构造函数的调用:
(1)全局变量:程序运行前;
(2)函数中静态变量:函数开始前;
(3)函数参数:函数开始前;
(4)函数返回值:函数返回前;
析构函数的调用:
(1)全局变量:程序结束前;
(2)main中变量:main结束前;
(3)函数中静态变量:程序结束前;
(4)函数参数:函数结束前;
(5)函数中变量:函数结束前;
(6)函数返回值:函数返回值被使用后;注意:对于相同作用域和存储类别的对象调用析构函数和构造函数的顺序的调用次序刚好相反
3.复制构造函数
1.复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性。复制构造函数创建一个新的对象,作为另一个对象的拷贝。复制构造函数只含有一个形参,而且其形参是对应对象的引用。复制构造函数形如 X::X( X& ), 只有一个参数即对同类对象的引用,如果没有定义,那么编译器生成缺省复制构造函数。
复制构造函数的两种原型(prototypes),以类Date为例,Date的复制构造函数可以定义为如下形式:Date(Date & );
或者:
Date( const Date & );
特别要注意的是:复制构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用 。这是因为函数传参数时实参是形参的一份拷贝下面我们来看这个例子:
class Date public: Date(int year = 1900, int month = 1, int day = 1) _year = year; _month = month; _day = day; Date(const Date& d) _year = d._year; _month = d._month; _day = d._day; private: int _year; int _month; int _day; ; int main() Date d1; Date d2(d1); return 0;
复制构造函数的特性:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
3.若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
3.复制构造函数的调用时机:、
1.一个对象需要通过另外一个对象初始化
2.一个对象以值传递的方式传入函数体
3.一个对象以传值的方式从函数返回
下面我们来看一下例子:
1.一个对象需要通过另外一个对象初始化
#include <iostream> using namespace std; class Complex public: Complex(double r, double i) real = r; imag = i; Complex(Complex& c) real = c.real; imag = c.imag; cout << "copy constructor!" << endl; private: double real; double imag; ; int main() Complex c1(1, 2); //调用构造函数Complex(double r, double i) Complex c2(c1); // 调用复制构造函数Complex( Complex & c) Complex c3 = c1; // 调用复制构造函数Complex( Complex & c) return 0;
2.一个对象以值传递的方式传入函数体
函数的形参是类的对象,调用函数时,进行形参和实参的结合。
如果某函数有一个参数是类Complex的对象,那么该函数被调用时,类Complex的复制构造函数将被调用。
复制代码 void func(Complex c) ; int main( ) Complex c1(1,2); func(c1); // Complex的复制构造函数被调用,生成形参传入函数 return 0;
3.一个对象以传值的方式从函数返回
当对象传入函数的时候被隐式调用以外,复制构造函数在对象被函数返回的时候也同样的被调用。换句话说,你从函数返回得到的只是对象的一份拷贝
Complex func() Complex c1(1,2); return c1; // Complex的复制构造函数被调用,函数返回时生成临时对象 ; int main( ) func(); return 0;
注意:
对象之接用等号赋值并不会调用复制构造函数。C++中,当一个新对象创建时,会有初始化的操作,而赋值是用来修改一个已经存在的对象的值,此时没有任何新对象被创建。初始化出现在构造函数中,而赋值出现在operator=运算符函数中。编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用复制构造函数。
#include <iostream> using namespace std; class Complex public: Complex(double r, double i) real = r; imag = i; Complex(Complex& c) real = c.real; imag = c.imag; cout << "copy constructor!" << endl; private: double real; double imag; ; int main() Complex c1(1, 2); Complex c2 = c1;//调用复制构造函数 Complex c3(3, 4); c3 = c1;//调用赋值构造函数 return 0;
4.匿名对象的去和留:
我们知道在C++的创建对象是一个费时,费空间的一个操作。有些固然是必不可少,但还有一些对象却在我们不知道的情况下被创建了。通常以下三种情况会产生临时对象:
1,以值的方式给函数传参;
2,类型转换;
3,函数需要返回一个对象时;
我们先来说结论:
结论1:函数的返回值是一个元素(复杂类型),返回的是一个新的匿名对象(所以会调用匿名对象类的copy构造函数)
结论2:匿名对象的去和留 .如果用匿名对象初始化 另外一个同类型的对象,匿名对象转成有名对象 //如果用匿名对象赋值给 另外一个同类型的对象,匿名对象被析构
3.如果直接定义一个匿名对象则定义完之后它就会被析构
下面我们来看代码:
#include <iostream> using namespace std; class Complex public: Complex(double r, double i) real = r; imag = i; Complex(Complex& c) real = c.real; imag = c.imag; cout << "copy constructor!" << endl; ~Complex() cout << "析构函数调用" << endl; private: double real; double imag; ; int main() Complex(1, 2); return 0;
我们通过调试可以发现当他定义完之后执行下一句语句的时候就调用了析构函数
下面我们再来看:
#include <iostream> using namespace std; class Complex public: Complex(double r, double i) cout << "构造函数调用" << endl; real = r; imag = i; Complex(const Complex& c) real = c.real; imag = c.imag; cout << "复制构造函数调用!" << endl; ~Complex() cout << "析构函数调用" << endl; private: double real; double imag; ; Complex test() Complex a(1, 2); return a; int main() Complex c1 = test(); return 0;
此时在test里面定义了一个对象那么就会调用他的构造函数初始化函数返回时会生成一个匿名对象调用匿名对象的复制构造函数,在析构a对象。而我们又恰好用一个未初始化的对象来接这个函数的返回值那么他就会转成有名对象也就是c1,进而在析构。
运行结果:
但如果我们用已经初始化过的对象来接那么他就会直接析构:
#include <iostream> using namespace std; class Complex public: Complex(double r, double i) cout << "构造函数调用" << endl; real = r; imag = i; Complex(const Complex& c) real = c.real; imag = c.imag; cout << "复制构造函数调用!" << endl; ~Complex() cout << "析构函数调用" << endl; private: double real; double imag; ; Complex test() Complex a(1, 2); return a; int main() Complex c1(2, 1); c1 = test();//匿名对象会直接析构掉 return 0;
运行结果:
4.深浅拷贝
什么是深拷贝和浅拷贝了?
浅拷贝就是通过赋值的方式进行拷贝,那为什么说这是浅拷贝呢?就是因为赋值的方式只会把对象的表层赋值给一个新的对象,如果里面有属性值为数组或者对象的属性,那么就只会拷贝到该属性在栈空间的指针地址,新对象的这些属性数据就会跟旧对象公用一份,也就是说两个地址指向同一份数据,一个改变就会都改变,析构时也会析构两遍从而会让程序崩溃
深拷贝则不会出现上述问题,重新开一块空间,拷贝数据
下面我们来看一下例子:
#include <iostream> using namespace std; class Student private: int num; char* name; public: Student(); ~Student(); ; Student::Student() name = new char[2]'2','0'; cout << "Student" << endl; Student::~Student() cout << "~Student " << (int)name << endl; delete name; name = NULL; int main() // 花括号让s1和s2变成局部对象,方便测试 Student s1; Student s2(s1);// 复制对象 system("pause"); return 0;
我们会发现此时程序崩溃了
由于s1和s2中的name同时指向了同一块空间当我们析构的时候同一块空间析构两次此时程序会崩溃
此时我们就需要自己写复制构造函数重新开辟一块新的空间:
#include <iostream> using namespace std; class Student public: Student(const char*str); ~Student(); Student(const Student& s) char* tmp = new char[strlen(s.name) + 1]; strcpy(tmp, s.name); name = tmp; private: int num; char* name; ; Student::Student(const char*str) num = 0; char*tmp = new char[strlen(str) + 1]; strcpy(tmp, str); name = tmp; cout << "Student" << endl; Student::~Student() //cout << "~Student " << name << endl; delete []name; name = NULL; int main() Student s1("hehe"); Student s2(s1);// 复制对象 return 0;
当我们写了复制构造函数时重写开辟了空间此时就不会出现崩溃的问题了
总结:
1.当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数
2.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
3.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。4.浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。
4..赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)注意:不能通过连接其他符号来创建新的操作符:比如operator@ 重载操作符必须有一个类类型或者枚举类型的操作数 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 操作符有一个默认的形参this,限定为第一个形参.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现
赋值运算符主要有四点:
1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。(按字节拷贝就是原封不动的拷贝过去,其实就是浅拷贝)
同样赋值运算符也存在和赋值构造函数相同的问题就是当成员变量的含有指针成员时使用编译器自己生成的赋值运算符只是简单的值拷贝当析构时会让同一块空间析构两次从而导致程序崩溃
#include <iostream> using namespace std; class Student public: Student(const char*str); ~Student(); Student(const Student& s) char* tmp = new char[strlen(s.name) + 1]; strcpy(tmp, s.name); name = tmp; private: int num; char* name; ; Student::Student(const char*str) num = 0; char*tmp = new char[strlen(str) + 1]; strcpy(tmp, str); name = tmp; cout << "Student" << endl; Student::~Student() //cout << "~Student " << name << endl; delete []name; name = NULL; int main() Student s1("hehe"); Student s2("helloworld"); s1 = s2;//使用编译器默认生成的赋值运算符 return 0;
此时我们没有写operator=当前使用的时编译器默认生成的。
运行结果:
与赋值构造函数相同都是同一块空间被析构两次所造成的:
析构两次:
此时我们需要自己写operator=进行深拷贝来解决:
#include <iostream> using namespace std; class Student public: Student(const char*str); ~Student(); Student(const Student& s)//复制构造函数深拷贝 char* tmp = new char[strlen(s.name) + 1]; strcpy(tmp, s.name); name = tmp; Student& operator=(const Student& s) if (&s != this) char* tmp = new char[strlen(s.name) + 1];//开空间 strcpy(tmp, s.name);//拷数据 delete[]name;//释放旧空间 name = tmp; return *this; private: int num; char* name; ; Student::Student(const char*str) num = 0; char*tmp = new char[strlen(str) + 1]; strcpy(tmp, str); name = tmp; cout << "Student" << endl; Student::~Student() //cout << "~Student " << name << endl; delete []name; name = NULL; int main() Student s1("hehe"); Student s2("helloworld"); s1 = s2; return 0;
operator总结:
当类中的程序变量中含有指针变量时我们需要考虑是否要显示的提供赋值运算符重载函数(即自定义赋值运算符重载函数):
用类 A 类型的值为类 A 的对象赋值,且类 A 的数据成员中含有指针的情况下,必须显式提供赋值运算符重载函数
最后:
最后:🙌🙌🙌🙌
结语:对于个人来讲,在编程上进行探索以及总结知识是一件有趣的时间,一个程序员,如果不喜欢编程,那么可能就失去了这份职业的乐趣。刷到我的文章的人,我希望你们可以驻足一小会,忙里偷闲的阅读一下我的文章,可能文章的内容对你来说很简单,(^▽^)不过文章中的每一个字都是我认真专注的见证!希望您看完之后,若是能帮到您,劳烦请您简单动动手指鼓励我,我必回报更大的付出~
以上是关于类和对象万字总结的主要内容,如果未能解决你的问题,请参考以下文章
❤️最后的大爆发❤️五万字总结SpringMVC教程——三部曲封神之作(建议收藏)