一、this指针与常函数
成员函数是如何区别调用它的对象?
#include <iostream>
using namespace std;
class Test
{
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、所以在成员函数中可以区分是哪个对象调用了它,并且在成员函数内访问成员变量时,编译器帮我们隐式在每个成员变量前增加了->,所以可以区别出每个对象的成员变量。
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(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 类名;
常对象的特点:
使用常对象调用成员函数时,编译器计算出的对象地址具有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指针,传递成员函数,完成函数调用。
C语言中的cosnt与C++中的const的区别?
二、拷贝构造函数和赋值函数
什么是拷贝构造
是一种特殊构造函数,如果没有显式的实现,编译器就会自动生成。
class 类名
{
public:
// 拷贝构造
类名(类名& 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;
// 把旧对象的指针变量所指向的内存拷贝给新对象的指针变量所指向的内存,如果不方便解引用时可以使用memcpy函数
*ptr = *that.ptr;
}
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=(类名& 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 的错误。
三、静态成员
普通成员
普通成员变量的特点
每创建一个对象,就分给该对象一块内存,里面存储成员变量,每多一个对象就多一份成员变量。
普通成员函数的特点
成员函数的参数列表中隐藏一个this指针,当通过对象调用成员函数时,编译器会自动计算出对象的地址隐式的传递给this
只能通过类对象才能调用成员函数。
静态成员
静态成员变量
被static修饰过的成员变量叫静态成员变量。
静态成员的特点和局限性
1、静态成员只能在类内声明,定义和初始化必须在类外(分配内存),相当于把一个全局变量的作用域限制到类内
2、静态成员使用的是data或bss内存段,所以类中的静态成员只有一份,所有类对象共用这一份静态成员
静态成员函数的特点和局限性
1、静态成员函数的参数列表中没有隐藏的this指针
2、静态成员函数中不能访问成员变量,也不能调用其他成员函数,但可以访问静态成员,也可以调用其他静态成员函数
3、静态成员函数可以使用 类名::函数名(实参)访问,不需要通过类对象,虽然也可以通过类对象调用,但能访问对象的成员变量
静态成员的作用
1、可以把类对象的共用成员设置成静态成员,这样可以达到节约内存的目的。
2、静态成员函数相当给所有对象提供了一个统一的管理接口,可以直接访问静态成员函数对类对象进行管理和设置
静态成员的作用
1、静态成员变量相当于把普通全局变量的作用域限制到类内,如果它的访问权是public,就可以当全局变量使用
2、可以把类对象