bool a = (3 > 2);
if (a == true)
typedef int BOOL
sizeof(bool) c++中bool类型占一个字节
sizeof(BOOL) C语言中通过宏定义的BOOL类型占四个字节
当程序中出现有数值不变,并且需要多次重复使用的变量时,就可以使用常量来表示
#define PI 3.14
宏定义在程序中是直接进行替换,而不进行类型检查
const float FPI = 3.14f;
常量的值一旦定义就不可以修改,因此在定义常量时一定要进行赋值;
变量的值可以进行修改
const float FPI = 3.14f;
float r = 0.5;
float* p = (float*) & FPI;
#FPI为常量,直接取地址&FPI获得的也是常量指针,而float* p是float类型的指针,不能直接将常量float指针赋值给float类型指针,因此需要做强制类型转换
#同样也因为常量指针的指向是不可以修改的,所以必选强转为float*
通过修改指针指向的地址来修改常量的值
int main()
{
const int nConst = 5;//已知数据类型的值
//在程序运行期间即使通过指针修改了常量的值,打印出来的仍然是5,因为编译器在编译时进行了优化
printf("nConst = %d\r\n",nConst);//因此在这里进行了直接替换
return 0;
}
int main(int nArgc, char* pArgv[])
{
const int nConst = nArgc;//程序在编译时并不知道nArgc的数值,不会进行优化
//此时在程序运行期间修改const的值,打印输出也会修改,因此此时编译器在编译时无法进行优化
printf("nConst = %d\r\n",nConst);
return 0;
}
int main()
{
char* p = "hello world!"; //该指针所指向的地址存放的是一个常量字符串,常量字符串不应该被修改
p[0] = 'a';//通过指针,修改指针p所指向的第一个元素
//编译通过,单运行时会报错,“hello world”字符串是一个常量,在内存中分配的空间应该是一个只能够读不能够写的内存空间,不可以对其进行修改
return 0;
}
int main()
{
const char* p = "hello world!";
p[0] = 'a';//编译器提示不可以修改
return 0;
}
const int n = 5;
int* p = const_cast(&n);
int a = 5;
int& n = a;
改变n的值,a的值也会变,改变a的值,n的值也会变,即a和n都是指向同一片内存空间
int& m = 5;错误,引用本身就是给变量起别名,怎么能用常量进行初始化
const int& k = 5;常量的引用可以使用常量进行初始化
int n = 5;
int m = 6;
int& refN = n;//引用refN与n建立关系
refN = m;//因此这里是赋值,而不再是将m与引用refN建立关系
作为函数参数使用
作为函数返回值使用,但是需要特别注意,函数体内部定义的变量一旦出了函数的作用域就会被释放,或者将内存空间分配给其他变量进行使用,而此时将函数体内部的变量作为返回值返回,在函数体外部进行修改本质上是在修改已经释放或者分配给其他变量的内存空间,存在着安全隐患
全局作用域,命名空间作用域
局部作用域,块作用域
类域(class)
在不同作用域定义多个相同名字的变量时会存在数据隐藏的现象
内部作用域的变量会隐藏外部作用域的变量(即相同名字的变量其会从内向外查找,如果最外部仍找不到它的值,程序就会报错)
namespace shellMad
{
int width = 5;
}
namespace shellMad
{
int height = 6;
}
namespace shellMad
{
int height = 6;
namespace neibu
{
int neibubianliang = 10;
}
}
namespace shellMad
{
int height = 6;
}
namespace n = shellMad;//给shellMad命名空间起一个别名n
n::height = 7;
::width 直接使用 ::加变量名 表示全局作用域中的width变量(四饼)
1.直接使用命名空间::变量名/函数名(推荐这种使用方式)
2.using namespace ShellMad;从这句话开始后面的变量默认都是使用ShellMad命名空间中的变量或函数(不推荐使用,这种写法仍然可能会造成冲突)
3.声明只使用命名空间中的部分变量和函数 using ShellMad::n;
4.::变量名/函数名 表示使用全局作用域中的变量
编译原理
.c / .cpp —cl.exe(编译器)—> .obj —linker.exe(链接器) —>.exe
命名空间中的变量在编译生成.obj文件的过程中会附加变量的命名空间以及作用域信息,因此可以通过命名空间区分不同的变量
通过名称粉碎来实现不同命名空间变量的区分
undname -f ?foo@ShellMad@@YAXXZ
1.函数名必须相同
2.函数参数个数,类型,顺序不同
3.返回值类型不同不能构成重载,调用约定不同不能构成函数重载
4.作用域必须相同
1.根据函数名找对应的函数,作为候选函数
-----1.1如果候选函数的个数为0,则报未定义错误(找不到标识符)2.如果候选函数的个数>0,从候选中找到匹配的函数(完全匹配,可以转换的匹配(char <-> int ,float <-> double, float <-> int等)
----- 2.1如果匹配的函数个数 == 0,则错误(隐式转换失败)
------2.2如果匹配的函数个数 > 0,找最佳匹配3.最佳匹配的个数 = 1, 就会调用该函数
最佳匹配的个数 > 1,就会报二义性
与命名空间的本质相同,是通过名称粉碎来区分不同的重载函数
1.封装
2.继承
3.多态
对象 = 数据(数据类型) + 行为(函数)
C++中结构体和类的唯一区别就是访问权限不同
访问权限:指在当前类域之外访问类的规则
1.public:公有权限,类内和类外均可访问
2.protected:保护权限(继承中使用)
3.private:私有权限(类域内部可以访问,但类域外部不可访问)
结构体的默认访问权限是公有权限
(默认访问权限就是指没有写明访问权限时)
类的默认访问权限是私有权限
1.类名一般加一个大写的C,例如:class CClock{};
2.函数成员(member)名前一般加上m,例如:int m_hour = 10;
3.类中主要包含两部分:数据成员和函数成员,数据成员一般写私有,函数成员一般部分公有,部分提供给外部进行使用
4.数据成员私有的原因是:类本身就是一种封装,数据成员属于类自身的属性,我们不希望外界直接对对象的属性进行修改
5.函数成员部分公有的原因是:既然类中的数据成员是私有的,外界无法直接访问,那么我们如果希望修改对象的属性就只能通过类中提供的函数进行get或set,因此类中会提供部分公有函数成员
6.类的最标准的写法:
-----6.1类中的数据成员仍然正常定义,类的函数成员中只写函数声明,函数的定义在外部书写,但是需要注意在类的外部书写函数定义时一定要加上类域
-----6.2一般将函数定义(定义内只写成员函数的声明)放到头文件中,而将类中函数成员的定义放到.cpp文件中,以时钟为例,这个类就存在两个文件:clock.h文件和clock.cpp文件,在使用clock类时我们只需要将clock.h的头文件include进来就可以正常使用
// clock.h文件
class CClock
{
private:
int m_hour = 10;
public:
int getHour(){};
};
// clock.cpp文件
#include"clock.h"
CClock::getHour()
{
return m_hour;
}
//使用clock类
#include"clock.h"
CClock clock;//定义对象
hour = clock.getHour();
在类的编译时期进行访问权限的检查,而在类的运行时期不做检查,因此可以通过指针的方式修改对象的私有属性
CClock clock;
clock.m_hour = 10;//编译器报错,类的私有属性不可以修改
*(int*)&clock = 10;//编译通过,此时类中的m_clock的值被修改为10
//类的大小
class CClock
{
private:
int m_hour = 1;
int m_minute = 2;
int m_second = 3;
};
classSize = sizeof(CClock);
int(*p)(int, int);
(1)*p表示该变量是一个指针
(2) int 表示该函数指针所指向的函数的返回值为int类型
(3) (int , int)表示该函数指针所指向的函数的形参为两个int型的变量
//C语言中模拟C++中类的封装
typedef void (*PFN_SetHour)(int n);
struct CClock
{
int m_hour = 1;
int m_minute = 2;
int m_second = 3;
//C语言中使用函数指针来代替函数成员
PFN_SetHour pfnSetHour;
};
void SetHour(struct CClock* cl, int n)
{
cl->m_hour = n;
}
int main()
{
//首先创建结构体变量
struct CClock cl;
cl.pfnSetHour = SetHour;//首先给结构体中的函数指针赋值
cl.pfnSetHour(&cl,1);//这里注意使用函数指针调用函数时,函数并不知道调用该函数的当前对象是谁,因此在调用该函数时必须调用对象的地址也传进去
return 0;
}
(1)数据成员是独立的
(2)函数成员是公用的
其实C++中当对象调用函数成员时,会偷偷的向函数中传入一个this指针,通过寄存器ecx传递,这种传递方式称之为thiscall
//clock.h
class CClock
{
private:
int m_hour = 10;
public:
void setHour(int n){};
};
//clock.cpp
#include"clock.h"
CClock::setHour(int n)
{
m_hour = n;//这是我们之前的写法
this->m_hour = n;//其实类的不同对象在调用函数成员时偷偷传入了一个this指针
}
//func.cpp
#include"clock.h"
//创建类域的函数指针
typedef int(CClock::*PFN_GETHOUR)(void);
int main()
{
CClock clock1;
CClock clock2;
//通过类域中的函数指针获得类域中函数的地址,单步调试发现在clock1和clock2调用setHour函数时,函数的地址并没有发生改变,说明类中的成员函数是公有的
PFN_GETHOUR lp = &CClock::setHour;
clock1.setHour(10);//不同对象在调用setHour时给this指针赋予了不同的地址
clock2.setHour(20);
return 0;
}
CClock clock1;//类对象
CClock* clock2;//类对象指针
typedef int(CClock::*PFN_GETHOUR)(void);
PFN_GETHOUR lp = &CClock::setHour;
(clock1.*lp)();//类的对象调用类域成员函数
(clock2->*lp)();//类的对象指针调用类域成员函数
(1)函数名必须是类名
(2)不写函数的返回值类型
(3)可以有多个参数
(4)可以没有参数
(1)构造函数允许函数重载
(2)如果类中没有将构造函数进行重载,编译器会提供一个默认构造函数(无参构造,可能会被优化)
(3)当构造函数只有一个参数时,该函数既表示构造函数,又表示一种隐式转换从而支持:CTest obj = 1;这种写法,在实际使用中可以通过explicit关键字表示当前的构造函数只支持显示的调用构造函数,不允许隐式转换
(4)CTest() = default; //显式的指明使用默认的构造函数
(5)CTest() = delete; //表示禁止使用默认构造函数
class CTest
{
//无参构造函数
CTest();
//有一个参数的构造函数
CTest(int n)
{
};
//有多个参数的构造函数
CTest(int n, char* name)
{
};
}
int main()
{
//调用无参构造函数
CTest t1;
//调用一个参数的构造函数
CTest t2 = 10;
CTest t3 (10);
//调用多个参数的构造函数
CTest t4 (10,"mike");
CTest t5 = CTest (1,"mike");
}
(1)类名前面加上~
(2)通常是由编译器决定调用时机,不需要手动调用
(3)析构函数不能有参数和返回值
(4)析构函数不能函数重载(构造函数可以函数重载)
//malloc创建一个堆空间
//使用malloc进行堆空间的分配,首先必须指明要分配的堆空间的大小,然后因为malloc返回的是void指针,因此需要将其强转为不同类型的指针
m_szName = (char*)malloc(255);//这里就可以说明new与malloc的区别
//堆空间释放
free(m_szName);
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
默认情况下系统会自动为我们调用一次析构函数,因此一般情况下我们不要自己手动调用析构函数,否则会造成内存空间的重复释放,报错
构造和析构函数默认情况下必须是公有访问权限
为什么不能是私有访问权限呢?
(1)如果构造函数是私有访问权限,在类域外部创建类的对象时对于类的构造函数没有访问权限,此时也就无法创建对象
(2)如果析构函数是私有访问权限,当对象消亡时同样无法释放内存空间
先讲一句提纲挈领的:构造和析构函数的调用都是为了使用而服务的,在进入作用域之前,构造函数需要被调用,在出作用域之后,析构函数会对内存进行释放
(1)栈上的局部对象的调用时机:
构造:声明该对象时构造
析构:对象出作用域时调用析构
(2)全局对象的调用时机:
构造:进入main函数之前调用
析构:出main函数之后调用
拷贝构造函数本质上也是一种构造函数
当用一个对象创建另外一个对象时调用
CTest m(10,"nike");
CTest n = m;//使用对象m来创建对象n
完全的把对象1拷贝给对象2,memcpy,默认拷贝构造函数是一种浅拷贝
class CTest
{
CTest(){};//构造函数
CTest(CTest& obj){};//自定义拷贝构造函数
CTest(CTest& obj) = default;//使用默认的拷贝构造函数
CTest(CTest& obj) = delete; //不使用默认的拷贝构造函数
class CTest
{
CTest()
{
//构造函数中有内存空间的开辟
m_size = (char*)malloc(255);
}
~CTest()
{
//析构函数中有内存空间的释放
free(m_size);
}
}
int main()
{
CTest t1(10,"nike");
CTest t2 = t1;
}
在上面这个例子中,调试程序几回发现,创建t1对象时调用了一次构造函数,创建t2对象时调用了默认的拷贝构造函数,在出main函数之前编译器会默认调用两次析构函数释放t1和t2的内存空间,但是由于默认拷贝构造函数是浅拷贝,即t1和t2实际是使用同一块内存空间,因此两次释放内存空间会报错
class CTest
{
CTest(){};
CTest(CTest& obj)
{
//浅拷贝的实现方式
this->m_size = obj.m_size;
//深拷贝的实现方式
m_size = (char*)malloc(255);
if(m_size == nullptr)
{
return;
}
memcpy(this->m_size,obj.m_size,255);
}
}
//本质是在堆上分配内存空间,并将其地址转化为char类型
char* pBuf = (char*)malloc(10);
//C语言中在堆上给对象分配内存空间
CTest* pBuf = (CTest*)malloc(sizeof(CTest));
但是使用malloc为对象在堆上分配内存空间的方式存在以下问题:
(1)并不会调用类的构造和析构函数,因此无法完成对象的初始化和资源释放
(2)使用malloc在堆上分配内存空间的本质其实就是在堆上开辟了内存地址并将其赋值给类指针,与普通对象在堆上开辟内存空间没有区别
new:先分配空间,然后调用构造函数进行初始化
delete:先调用析构函数,然后再释放空间
//使用new和delete为对象分配和释放堆上的内存空间
CStudent* pStu = new CStudent;
if(pStu != nullptr)
{
delete pStu;
}
int* pN = new int(123);
delete pN;
问题1:C++中的new与delete和C语言中的malloc与free有什么区别呢?
答:new与delete和malloc与free在对普通变量进行分配和释放时没有任何区别,并且可以混用,但是在为对象分配和释放堆空间时,new和delete会调用类的构造和析构函数,而malloc和free则不会
//混用1:
char* pN = (char*)malloc(sizeof(char));
delete pN;
//混用2:
char* pN = new char;
free(pN);
问题2:如何在堆上为数组分配和释放内存空间呢?
答:
(1)对于普通数据类型变量
//使用malloc和free
int* pN = (int*)malloc(sizeof(int) * 10);
free(pN);
//使用new和delete
int* pN = new int[10];
delete [] pN;
(2)对于对象而言:
CTest* pBuf = new CTest[10];
delete [] pBuf;
假如我们需要一个学生类和一个老师类,学生类和老师类中有许多重复的属性,为了避免在代码中重复的定义,我们可以创建一个人类,然后将学生类和老师类公有的属性写在人类中,通过继承的方式创建学生类和老师类
子类 ---------------父类
派生类-------------基类
class CPerson
{
//构造,析构,get,set,数据成员
}
class CStudent : public CPerson
{
}
class CTeacher : public CPerson
{
}
public:类域内和类域外均可访问
protected:类域本身和其派生类可以访问,其他类和类域外部均不可访问
private:类域内可以访问,类域外部不可以访问
父类 | 继承方式 | 子类 |
public | public继承 | public |
protected | public继承 | protected |
private | public继承 | 不可见 |
public | protected继承 | protected |
protected | protected继承 | protected |
private | protected继承 | 不可见 |
public | private继承 | private |
protected | private继承 | private |
private | private继承 | 不可见 |
继承关系与基类中的可见性中取其最严格的部分
(1)子类从父类中继承是将父类中的所有数据成员都继承过来,这一点可以通过sizeof查看内存大小来验证
(2)继承的可见性只是编译器在编译时进行的检查
(3)既然可见性是编译器在编译时做的检查,那么在程序运行时期,我们就可以通过指针的方式来修改子类从父类中继承而来的不可修改的数据成员
(4)继承的可见性实际上只是编译器的限制,是编译器一种善意的做法,在使用时尽量不要使用指针绕过编译器的限制来进行数据成员的修改
CStudent stu;
int* p = (int*)((char*)&stu + 8);
*p = 123;//通过指针将stu对象中原本不可访问的数据成员的值进行了修改
CPerson per;//父类创建对象
CStudent stu;//子类创建对象
CPerson* pPer = &stu;//子类转换为父类
CStudent* pStu = (CStudent*)&per;//父类转换为子类
(1)子类转父类是安全的,子类中的数据成员更多,内存空间更大,转换后相当于可访问的内存空间变小
(2)父类转子类是不安全的,父类中数据成员更少,内存空间更小,转换后子类可以访问到不属于父类的内存空间,会存在越界访问
class CStudent : public CPerson
{
}
class CTest
{
private:
CPerson per;//成员类
}
(1)构造:先父类,再子类
(2)析构:先子类,再父类
(1)构造:先成员类,再自己
(2)析构:先自己,再成员类
(1)构造:先父类,再成员类,最后子类
(2)析构:先自己,再成员类,再父类
(1)用于调用父类中的有参构造
(2)用于自身成员的初始化
(3)用于常量成员的初始化
class CStudent : Public CPerson
{
//没有初始化列表的构造函数
//CStudent()
//{
//}
//有初始化列表的构造函数
CStudent():CPerson(110,4),m_nStuID(10),n(12)
{
}
private:
int m_nStuID;
const int n;//也可以直接写const int n = 10;
}
class CPerson
{
public:
void test(){};//父类中的成员函数
private:
int m_test = 20;//父类中的数据成员
};
class CStudent
{
public:
void test(){};//子类中的成员函数
private:
int m_test = 10;//子类中的数据成员
};
int main()
{
CStudent stu;
stu.m_test = 1;//修改的是子类中的数据成员
stu.test();//调用的是子类中的成员函数
}
总结:
(1)类中的数据隐藏和函数隐藏都是由内向外的,当从子类中找到数据成员或者函数成员时,就不会再去找父类中的
(2)构成函数隐藏的条件:
作用域不同
函数名相同
返回值类型,参数列表,调用约定均不作考虑
函数重载 | 函数隐藏 |
函数名相同 | 函数名相同 |
作用域相同 | 作用域不同 |
参数列表的顺序,类型不同也可以构成函数重载 | 参数列表的顺序,类型不同不可以构成函数隐藏 |
调用约定不同不构成函数重载 | 调用约定不同不构成函数隐藏 |
一个父类:人类,有一个功能:说人话
两个子类:中国人类,英国人类,在这两个子类中都重写了说话的功能函数,实现函数隐藏
目标:一群人在说话,创建一个对象指针数组,希望不同对象调用说话功能函数时,都可以使用各自的说话函数
例如:中国人:说中文;英国人:说英文
而不是都调用父类中的功能函数:说人话
class CPerson
{
public:
virtual voidty speak() {};
}
class CPerson
{
public:
virtual void speak()
{
printf("说人话\r\d");
};
private:
int ID = 10;
};
class CStudent:public SPerson
{
public:
virtual void speak()
{
printf("说中文\r\d");
};
};
int main()
{
CStudent stu;
}
增加virtual关键字 | |||
stu | |||
stu对象地址 | 前4个字节存放指针,该指针指向CStudent中的speak函数 | CStudent函数地址 | |
ID = 10 |
- 上面表格描述的就是一个虚函数表,当父类中的函数前加上关键字virtual时,子类对象的大小就会增加4个字节,这4个字节存放的是一个指针,该指针指向其对应的函数(例如中国人说中文)
- 虚函数的实现过程其实就是创建对象后,当需要调用对象对应的函数成员时,首先找到该对象的地址,然后找到该对象的前4个字节,通过该对象的前4个字节中存放的指针,找到对应的函数,然后调用,达到多态的效果
(1)虚函数的调用方法是间接调用,先查虚表地址,再查虚表中的虚函数指针
(2)增加了虚函数virtual关键字的对象头部4个字节是虚表地址(某些情况,单继承)
1.作用域不同(父子类之间的继承关系)
2.函数名,参数列表(参数类型,参数个数,参数顺序),调用约定(__thiscall),返回值类型必须相同
3.必须要有virtual关键字
1.为什么一定要有virtual关键字,我觉的virtual关键字其实更像是一个标识符,它既要写在父类中,也要写在子类中,凡是前面有virtual关键字的成员函数都会被放到虚函数表中
2.如果父类中有写virtual关键字,而子类中没有写virtual关键字,当子类继承父类时,虚函数表中只存在父类的虚函数,因此此时只能继承来自父类的虚函数
3.如果子类和父类中都写了virtual关键字,在创建子类对象,并调用其函数成员时就会存在函数覆盖的现象,即子类中虚函数覆盖了父类中的虚函数
1.函数重载:
(1)函数名称相同
(2)参数列表(个数,类型,顺序)不同
(3)作用域相同
(4)函数重载不考虑返回值,调用约定的不同
2.函数隐藏:
(1)函数名相同
(2)参数列表(个数,类型,顺序)不做考虑
(3)作用域不同
(4)函数隐藏不考虑返回值,调用约定的不同
3.函数覆盖:
(1)函数名相同
(2)参数列表(个数,类型,顺序)必须相同
(3)作用域不同(父子类之间继承关系)
(4)函数覆盖要求返回值,调用约定也必须相同
1.子类继承了所有的父类虚函数(公有,私有虚函数没有意义)
2.父类中进行虚函数定义时的顺序就决定了子类中虚函数的顺序
3.子类重写了父类的虚函数,则会在子类自己的虚表中覆盖对应位置的虚函数
4.子类如果未重写父类中的某个虚函数,则直接继承父类的该虚函数
5.子类自己定义的虚函数(父类中未定义)则会出现在子类虚表中所有父类虚函数的后面
(1)由编译器在编译时期确定的
(2)在运行时,内存中并没有虚函数个数的表示
(3)虚表并不是以00结尾的(视频中仅仅是VS编译器中的巧合)
根据函数名称,直接调用该函数(在编译器编译时候就已经确定)
(1)普通的函数调用
(2)对象的普通成员函数的调用
(3)对象的虚函数的调用
虚函数通过查找对象虚表下标来调用函数的方法(在运行时期确定调用谁)
(1)通过对象的指针调用虚函数
(2)通过对象的引用调用虚函数
class CTest()
{
public:
void foo1(){};
virtual void foo2(){};
};
void foo()
{
};
int main()
{
//下面这三个例子都是直接调用的例子
foo();//普通函数的调用
CTest test;
test.foo1();//对象普通函数成员的调用
test.foo2();//对象的虚函数的调用
//下面再举两个间接调用的例子
CTest* test1 = &test;
test1->foo2();//使用对象的指针通过查找续表地址来调用
CTest& test2 = test;
test2.foo2();//使用对象的引用通过查找虚表地址来调用
//第三个值得思考的例子
//当我们明确了类域范围后,不论是指针还是引用,此时的调用均是直接调用
test1->CTest::foo2();
test2.CTest::foo2();//不太确定有没有这种写法
}
调用者类型来决定查找的起点
(1)在调用者的类中,查找同名函数
(11)如果没有,则在上一层查找,如果均找不到,则报错(函数未定义错误)
(12)如果有,则不会再往上面查找,可见的域就是当前找到的同名函数所在的域(函数隐藏)
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
(2)在当前可见的域中,找到最佳函数(函数重载规则)
(21)如果唯一的最佳函数不是虚函数,那么该调用就是直接调用
(22)如果唯一的最佳函数是虚函数,判断该调用是否是指针或引用
(221)调用者是指针或引用,则是间接调用(函数覆盖规则)
(222)调用者不是指针或引用,则是直接调用
(223)调用者是指针或引用,但指定了类域,则也是直接调用
使得无法修改数据成员,一般用来修饰类中的get成员函数
让编译器提醒开发者函数不能修改类的成员变量,用于const对象(引用或指针)
class CTest
{
public:
char getNumber() const;//函数声明
};
char CTest::getNumber() const
{
return m_nNumber;
}
(1)普通的成员函数:函数内this指针类型:T* const(指针常量):表示指针本身不能被修改,但是可以修改指针指向的内容,也就是修改数据成员的值
(2)常成员函数:函数内this指针类型:const T* const
(2.1)该指针本身不能被修改
(2.2)该指针指向的内容也不能被修改
因此,常成员函数的本质就是将this指针变为const T* const类型,限制开发者对其内容和指向的修改
如果我们的需求是大部分的数据成员不能被修改,而部分成员函数确实有在成员函数中修改的需求,该怎么办呢?
(1)第一种实现方式:强行将this指针修改为T* const 类型(不安全,因为一旦修改this指针的类型,就意味着其他我们不想要修改的数据成员也可以被修改了)
(2)第二种实现方式:在需要被修改的数据成员前加上mutable关键字
class CInteger
{
public:
CInteger(int nNumber)
{
m_Number = nNumber;
}
int getNumber();//一个只写了函数声明
void SetNumber(int nNumber)//一个写了完整的函数实现
{
m_Number = nNumber;
}
private:
//mutable int m_Number; //添加mutable关键字可以进行修改
int m_Number;
};
int CInteger::getNumber()
{
m_Number = 1;//理论上来说,我们不希望get函数可以修改数据成员,因此常成员函数的需求就来了
//学过this指针我们知道m_Number的本质是this-m_Number = 1;
//因为普通成员函数中的this指针是T* cosnt类型,可以修改指针指向的内容,因此允许修改
return m_Number;
}
/*
int CInteger::getNumber() const
{
m_Number = 1;//此时修改编译器就会报错
//学过this指针我们知道m_Number本质是this->m_Number
//因为常成员函数内部的this指针为const T* const类型,我们既不能修改指针的指向,也不能修改指针指向的内容,因此无法修改
//那么如果我们希望强行修改呢?
//方法一:
CIntegr* const p = (Cinteger* const)this;
p->m_Number = 123;//此时可以修改
//方法二:
通过在类的成员变量定义时添加mutable关键字
m_Number = 1234;
return m_Number;
}
*/
int main(int argc, char* argv[])
{
CInteger i = 1;
return 0;
}
(1)可以用于初始化普通成员变量/常成员变量
(2)通常用来构造有参数的成员对象
class CTest
{
public:
CTest(int n, int m)
{
m_n = n;
m_m = m;
}
private:
int m_n;
int m_m;
};
class CInteger
{
public:
CInteger(int nNumber):
m_Num(10),//初始化列表给const成员变量初始化
m_Number(200),//初始化列表给普通成员变量进行初始化
m_t(1,2) //构造有参对象(初始化)
{
m_Number = nNumber;
}
private:
int m_Number;
const int m_Num;
//const成员变量要么在定义时直接初始化
//const int m_Num = 10;
CTest m_t;//将另一个类的有参对象作为该类的数据成员
};
首先回忆下,类的标准写法:
(1)类的定义写在头文件中,类中成员函数的实现写在cpp文件中
(2)头文件中只写类的成员函数的声明,cpp文件中写类的成员函数的实现
//头文件
class CTest
{
public:
CTest() = default;
int getNumber();//函数声明
public:
static int m_nStaic;//静态成员变量必须在cpp中写实现,静态成员变量和类的成员函数类似,是属于同一个类的所有对象所公用的
private:
int m_Number;
};
//cpp文件中
int CTest::getNumber()
{
return m_Number;
}
int CTest::m_nStatic = 10;//这里我们给静态成员变量进行了初识化,如果未进行初始化,编译器默认会赋值为0
int globalVar = 10;
int main()
{
CTest t;
t.m_nStatic = 100;
CTest::m_nStatic = 10;//不需要进行对象的实例化就可以使用静态成员变量
}
(1)静态成员变量和类的成员函数类似,都是同一个类的不同对象所共用的
(2)因此静态成员变量也要和成员函数类似,要单独把其实现写在类额外面
(3)如果我们未给静态成员变量赋值,默认情况下系统会用0对其进行初始化
(4)静态成员变量和全局变量非常类似,其本质就是一个带类域的全局变量