一、this指针与常函数
成员函数是如何区别调用它的对象?
#include <iostream> using namespace std; class Test { const int num; public: Test(int num):num(num) {} void show(void) { cout << num << " " << &num << endl; } }; int main(int argc,const char* argv[]) { Test t1(1234), t2(5678); t1.show(); t2.show(); }
C语言中我们如何解决该问题:
由于C语言中的结构没有成员函数,只能定义普通函数,取函数名时,让它与结构有关联,然后在函数的第一个参数把结构变量的地址传递过来,从而区别每个调用它的结构变量。
在C++语言中该问题由编译器帮忙解决:
1、C++语言中的结构、联合、类可以定义成员函数,成员函数都会有一个隐藏的参数。
2、该参数就是个地址,也叫this指针,可以显式使用。
3、使用结构、联合、类对象可以直接调用成员函数时,编译器会自动计算对象的地址隐式的传递给成员函数。
4、所以在成员函数中可以区分是哪个对象调用了它,并且在成员函数内访问成员变量时,编译器帮我们隐式在每个成员变量前增加了this->,所以可以区别出每个对象的成员变量。
5、由于成员函数参数列表中都隐藏着this指针,所以普通的成员函数无法作为回调函数使用。
#include <iostream> #include <pthread.h> #include <signal.h> using namespace std; class Test { int num; public: Test(int num):num(num){ } void* run(Test* this,void* arg) { } void sigint(int signum) { } void show(void) { cout << num << " " << &num << endl; } }; int main(int argc,const char* argv[]) { signal(SIGINT,Test::sigint); pthread_t tid; pthread_create(&tid,NULL,Test::run,NULL); return 0; }
6、显式使用this指针可以解决函数参数与成员变量同名的问题。
#include <iostream> #include <pthread.h> #include <signal.h> using namespace std; class Test { int num; public: Test(int num):num(num){ } void setNum(int num) { this->num = num; cout << this->num << " - " << &this->num << endl; } void show(void) { cout << num << " " << &num << endl; } }; int main(int argc,const char* argv[]) { Test t1(1234), t2(5678); t1.show(); t2.show(); t1.setNum(6666); t1.show(); }
常对象与常函数
什么是常函数
在成员函数的参数列表的末尾(小括号后面),用const修饰,这种成员就叫常函数。
class 类名 { public: // 常函数 返回值 函数名(参数列表) const { } };
常函数的特点:
常函数隐藏的this指针具有cosnt属性。
什么是常对象
在定义结构、联合、类对象时,使用const修饰,这种对象就叫常对象。
const 类名 对象名; const 类名* 指针变量 = new 类名;
常对象的特点:
使用常对象调用成员函数时,编译器计算出的对象地址(this)也具有const属性。
常对象有常函数的局限性:
1、常对象不能调用普通成员函数,只能调用常函数(构造函数和析构函数除外),但普通对象既可以普通成员函数,也可以调用常函数。
2、在常函数中不能显式修改成员变量,并且也不能调用普通的成员函数,只能调用常函数。
3、如果类的对象一定会被const修饰,那么它的成员函数都要定义为常函数。
4、如果类的对象可能被const修饰,也可能不修饰,那么它的成员函数要写两份,一份常函数,另一份普通成员函数。
mutable关键字的作用
如果常函数的const属性与函数的功能发生冲突,一定要修改成员变量,那么使用mutable关键字修饰一下需要在常函数中修改的成员变量。
#include <iostream> #include <pthread.h> #include <signal.h> using namespace std; class Test { mutable int num; public: Test(int num):num(num){ } void setNum(int num) const { this->num = num; cout << this->num << " - " << &this->num << endl; } void show(void) const { cout << num << " " << &num << endl; } }; int main(int argc,const char* argv[]) { const Test t(1234); t.show(); t.setNum(23456); t.show(); return 0; }
空类的对象为什么占用1字节内存?
1、因为C++中的空的结构、联合、类里面有隐藏的成员函数。
2、成员函数的参数列表中有隐藏this指针,调用这些成员函数时就需要计算出对象的地址传递给成员函数。
3、空的类对象至少要在内存中占据一个字节,才可以计算出this指针,传递成员函数,完成函数调用。
二、拷贝构造函数和赋值函数
什么是拷贝构造
是一种特殊构造函数,如果没有显式的实现,编译器就会自动生成。
class 类名 { public: // 拷贝构造 类名(const 类名& that) { } };
什么时候会调用拷贝构造
当使用一个类对象给另一个新的类对象初始化时,就会自动调用拷贝构造。
#include <iostream> using namespace std; class Test { public: Test(void) { cout << "调用了普通的构造函数" << endl; } Test(const Test& that) { cout << "调用了拷贝构造" << endl; } }; void func(Test t) { } int main(int argc,const char* argv[]) { Test t1; // 调用的是普通构造 Test t2 = t1; // 调用的是拷贝构造 func(t1); // 调用的是拷贝构造 return 0; }
拷贝构造的任务是什么
拷贝构造参数对象的所有成员变量挨个赋值给新对象的成员变量,一般情况下编译器自动生成的拷贝构造就能完全满足我们使用需求。
什么时候需要显式实现拷贝构造
当成员变量中有指针成员且指向了堆内存,就需要显式实现拷贝构造。
编译器自动生成的拷贝构造,只会对成员变量挨个赋值,如果成员变量中有指针变量且指向堆内存,结果就两个对象的指针变量同时指向一份堆内存,当它们执行析构函数时,会把这块堆内存释放两次,产生 double free or corruption 的错误。
正确的做法应该是先给新对象的指针变量重新申请一份堆内存,然后把旧对象的指针变量所指向的内存拷贝到新对象的指针变量所指向的内存。
#include <iostream> using namespace std; class Test { int* ptr; public: Test(int num) { ptr = new int; cout << "new:" << ptr << endl; *ptr = num; } ~Test(void) { cout << "delete:" << ptr << endl; delete ptr; } /* 编译器生成的拷贝构造,会造成 double free Test(const Test& that) { ptr = that.ptr; } */ Test(const Test& that) { // 给新对象的指针变量重新申请堆内存 ptr = new int(*that.ptr); // 把旧对象的指针变量所指向的内存拷贝给新对象的指针变量所指向的内存,如果不方便解引用时可以使用memcpy函数 } void show(void) { cout << "val:" << *ptr << " addr:" << ptr << endl; } }; int main(int argc,const char* argv[]) { Test t1(12345); Test t2 = t1; t1.show(); t2.show(); return 0; }
什么是赋值函数
是一种特殊的成员函数,如果没有显式实现,编译器会自动生成。
class 类名 { public: // 赋值函数 const 类名& operator=(const 类名& that) { } };
什么时候会调用赋值函数
当一个旧对象给另一个旧对象赋值时会自动调用赋值函数。
当一个旧对象给另一个新对象初始化时会自动调用拷贝构造函数。
#include <iostream> using namespace std; class Test { public: Test(const Test& that) { cout << "调用了拷贝构造" << endl; } void operator=(const Test& that) { cout << "调用了赋值函数" << endl; } }; int main(int argc,const char* argv[]) { Test t1; // 调用了普通的构造函数 Test t2 = t1; // 调用了拷贝构造 t1 = t2; // 调用的是赋值函数 return 0; }
赋值函数的任务是什么
赋值函数与拷贝构造的任务几乎相同,都是挨个给成员变量赋值,但如果需要显式实现时,它的业务逻辑不同。
什么时候需要显式实现赋值函数
当需要显式实现拷贝构造时,就需要显式实现赋值函数,它们两个面临问题是一样的。
赋值函数不应该对成员指针变量赋值,而应该对象成员指针变量所指向的内存进行拷贝。
#include <iostream> using namespace std; class Test { int* ptr; public: Test(int num) { ptr = new int; cout << "new " << ptr << endl; *ptr = num; } ~Test(void) { cout << "delete " << ptr << endl; // delete ptr; } Test(const Test& that) { ptr = new int; // 如果不方便解引用,可以调用memcpy函数进行拷贝 *ptr = *that.ptr; cout << "new " << ptr << "调用了拷贝构造" << endl; } const Test& operator=(const Test& that) { // 当ptr和that.ptr指向的内存块大小一样,可以直接进行内存拷贝 *ptr = *that.ptr; cout << "调用了赋值函数" << endl; return *this; /* 当对象的ptr指向的内存与与that.ptr指向的内存块不一样大 先释放旧的ptr 再分配新的,要与that.ptr的内存块一样大 然后再拷贝 */ } }; int main(int argc,const char* argv[]) { Test t1(1234); // 调用了普通的构造函数 Test t2 = t1; // 调用了拷贝构造 t1 = t2; // 调用的是赋值函数 return 0; }
浅拷贝与深拷贝
拷贝就是一个对象给另一个对象赋值,编译器自动生成的拷贝构造和赋值函数执行的业务逻辑就是浅拷贝(成员指针给成员指针赋值),深拷贝就是把成员指针所指向的内存拷贝给另一个成员指针所指向的内存。
浅拷贝就是指针给指针赋值,深拷贝就内存给内存赋值。
注意:如果成员变量中没有成员指针,则浅拷贝就可以满足需求,如果如果成员变量中有成员指针且指向堆内存,则必须手动实现深拷贝,否则就会出现 double free or corruption 的错误。
练习:
自定义MyString类,并实现构造、析构、拷贝构造、赋值等函数。
class MyString { char* cStr; public: MyString(const char* str="") { cStr = new char[strlen(str)+1]; strcpy(cStr,str); } ~MyString(void) { delete[] cStr; } MyString(const MyString& that) { cStr = new char[strlen(that.cStr)+1]; strcpy(cStr,that.cStr); } const MyString& operator=(const MyString& that) { if(this != &that) { delete[] cStr; cStr = new char[strlen(that.cStr)+1]; strcpy(cStr,that.cStr); } return *this; } };
三、静态成员
普通成员
普通成员变量的特点:
每创建一个对象,就分给该对象分配一块内存,里面存储成员变量,每多一个对象就多一份成员变量。
普通成员函数的特点:
成员函数的参数列表中隐藏一个this指针,当通过对象调用成员函数时,编译器会自动计算出对象的地址隐式的传递给this。
只能通过类对象才能调用成员函数。
静态成员
静态成员变量
什么是静态成员变量
被static修饰过的成员变量叫静态成员变量。
class Test { // 静态成员 static int num; public: void show(void) { cout << num << endl; } };
静态成员的特点和局限性:
1、静态成员只能在类内声明,定义和初始化必须放在类外。
2、静态成员使用的是data或bss内存段,所以类中的静态成员只有一份,所有类对象共用这一份静态成员。
3、如果类中有静态成员,计算类对象字节数时,静态成员不包含在内。
静态成员函数
什么是静态成员函数
被static修饰的成员函数叫静态成员函数。
class Test { static int num; public: void show(void) { cout << num << " " << &num << endl; } static void func(void) { } };
静态成员函数的特点和局限性:
1、静态成员函数的参数列表中没有隐藏的this指针。
2、静态成员函数中不能直接访问成员变量,也不能调用其它成员函数,但可以访问静态成员也可以调用其它静态成员函数。
3、静态成员函数可以使用 类名::函数名(实参) 调用,不需要通过类对象,虽然也可以通过类对象调用,但依然不能直接访问对象的成员变量。
4、静态成员函数的内部,也算是类内,虽然不能直接访问成员变量,但如果把类对象,作为参数传递给静态成员函数,那么它依然能访问成员变量。
静态成员的作用
1、静态成员变量就相当于把普通全局变量的作用域限制到类内,如果它的访问权限是public,就可以当全局变量使用,只是需要在变量名前面加 类名::静态成员变量。
2、可以把类对象的共用成员设置为静态成员变量,这样可以达到节约内存的目的,也可以作为类的管理信息。
3、静态成员函数相当于把普通函数的作用域限制到类内,当作给所有对象提供了一个统一的管理接口,可以不破坏类的封装性前提下访问静态成员,对类对象进行管理和设置(管理信息就是私有的静态成员变量)。
4、静态成员函数由于没有了隐藏的this指针,就可以作为回调函数使用了。
四、单例模式
什么是单例模式
只能创建出一个对象的结构、联合、类叫单例模式。
什么时候需要使用单例模式
1、Windows系统的任务管理器。
2、网络或服务器程序的访问计数器。
3、线程池、数据池,一个程序中只能创建一个线程池或数据池。
单例模式的实现原理
1、把构造函数、拷贝构造设置成私有的,不允许创建类对象。
2、在类中声明+定义一个静态的类对象(饿汉单例) 或类对象指针(懒汉单例)。
3、提供一个静态成员接口,用于获取静态的类对象。
饿汉单例模式
只要程序一运行,就把单例类的类对象创建出来,不管后续是否使用到。
缺点:如果后续程序使用不到单例对象,就造成了资源、时间上的浪费。
优点:线程安全,再多的线程也不可能创建出多个类对象。
懒汉单例
直到调用获取单例类对象的接口才会把对象创建出来。
缺点:如果在多线程情况下,多个线程同时调用对象的获取接口,可能会创建出多个类对象。
优点:如果用不到单例对象就不会真正创建,节约资源和时间。
单例模式的实现原理
1、把构造函数、拷贝构造设置成私有的,不允许创建类对象。
2、在类中声明+定义一个静态的类对象(饿汉单例) 或类对象指针(懒汉单例)。
3、提供一个静态成员接口,用于获取静态的类对象。
练习1:实现饿汉单例模式。
#include <iostream> using namespace std; class HungrySingle { // 声明静态对象 static HungrySingle obj; // 把构造和拷贝构造设置为private,可以防止创建类对象 HungrySingle(void) { cout << "创建了类对象" << endl; } HungrySingle(const HungrySingle& that) {} public: // 只能调用该接口获取静态类对象 static HungrySingle& getSingleObject(void) { return obj; } void show(void) { cout << this << endl; } }; // 定义静态对象 HungrySingle HungrySingle::obj; int main(int argc,const char* argv[]) { cout << "main run ..." << endl; HungrySingle& h1 = HungrySingle::getSingleObject(); HungrySingle& h2 = HungrySingle::getSingleObject(); HungrySingle& h3 = HungrySingle::getSingleObject(); h1.show(); h2.show(); h3.show(); return 0; }
练习2:实现懒汉单例模式。
#include <iostream> using namespace std; class LazySingle { static LazySingle* obj; LazySingle(void) { cout << "创建单例对象" << endl; } LazySingle(const LazySingle& that) { } public: static LazySingle* getSingleObject(void) { if(NULL == obj) { obj = new LazySingle; } return obj; } void show(void) { cout << this << endl; } }; LazySingle* LazySingle::obj; int main(int argc,const char* argv[]) { cout << "main run ..." << endl; LazySingle* l1 = LazySingle::getSingleObject(); LazySingle* l2 = LazySingle::getSingleObject(); LazySingle* l3 = LazySingle::getSingleObject(); l1->show(); l2->show(); l3->show(); return 0; }