目录
一,内存分配模型
1.程序运行时的内存四区
C++程序在执行时,将内存大方向划分为4个区域。这里需要注意的是,在程序编译之后就会生成exe的可执行程序,而在未执行该程序之前分为代码区和全局区两个区域;而在执行该程序之后才又分为栈区和堆区两个区域。
(1)代码区:存放函数体的二进制代码,有操作系统进行管理的。
(2)全局区:存放全局变量和静态变量以及常量。
(3)栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。
(4)堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
内存四区的意义:在不同区域存放的数据,为其赋予不同的生命周期,给我们更大的灵活编程的空间。
1.1 代码区
(1)存放CPU执行的机器指令;
(2)共享性。代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可,这样可以大大节省内存空间。
(3)只读性。代码区是只读的,这样可以防止程序意外地修改了它的指令,避免不必要的麻烦。
1.2 全局区
(1)存放全局变量和静态变量;
(2)全局区还包括了常量区,字符串常量和其他常量;
(3)该区域的数据在程序结束后由操作系统释放。
为了更加直观地感受到不同区域的数据的位置,可以分别创建全局变量,静态变量,常量去,字符串常量,执行以下代码得出结果来直观感受。
#include<iostream>
using namespace std;
//1.创建全局变量
int m = 10;
int n = 10;
//5.1 const 修饰的全局变量
const int e = 10;
const int f = 10;
int main()
{
//2.创建普通局部变量
int a = 10;
int b = 10;
cout<< "局部变量a的地址为:" << (int)&a << endl;
cout << "局部变量b的地址为:" <<(int) & b << endl << endl;
cout << "全局变量m的地址为:" << (int)&m << endl;
cout << "全局变量n的地址为:" << (int)&n << endl<<endl;
//3.创建静态变量
static int c = 10;
static int d = 10;
cout << "静态变量c的地址为:" << (int)&c << endl;
cout << "静态变量d的地址为:" << (int)&d << endl<<endl;
//4.创建字符串常量
string name1 = "li";
string name2 = "hu";
cout << "字符串常量“li”的地址为:" << (int)&name1 << endl;
cout << "字符串常量“hu”的地址为:" << (int)&name2 << endl<<endl;
//5.创建const修饰的常量
//5.1 const 修饰的全局常量
//5.2 const 修饰的局部常量
const int g = 10;
const int h = 10;
cout << "const 修饰的全局常量e的地址为:" << (int)&e << endl;
cout << "const 修饰的全局常量f的地址为:" << (int)&f << endl<<endl;
cout << "const 修饰的局部常量g的地址为:" << (int)&g << endl;
cout << "const 修饰的全局常量h的地址为:" << (int)&h << endl << endl;
system("pause");
return 0;
}
得到的结果如下:
可以看到,每种相同的变量之间的地址都隔得较近,当然,不同的变量相隔的距离就较远了,说明每一种变量都有它自己的存放区域。
同时可以知道:
局部变量与const修饰的局部变量是不在全局区中的;
全局变量,静态变量(static关键字),常量(字符串常量,const修饰的全局变量)是在全局区中的。
1.3 栈区
由编译器自动分配释放,存放函数的参数值,局部变量等。栈区的数据由编译器管理开辟和释放。
注意事项:不要返回局部变量的地址,因为栈区开辟的数据由编译器自动释放。
如下例子:
#include<iostream>
using namespace std;
//1.定义一个函数
int* func()
{
int a = 10; //定义一个局部变量
return &a; //返回局部变量的地址,用于测试
}
int main()
{
//2.接受函数的返回值
int* p = func();
//3.打印结果,测试结果是否正确
cout << "输出 *p 的结果是:" << *p << endl;
cout << "输出 *p 的结果是:" << *p << endl;
system("pause");
return 0;
}
理论上来说,两次得到的结果 应该是不一样的,但是由于我是在VS 2022上运行的,也许是运行的平台不同吧,我这里得到的结果是一样的,这里就不展示输出的结果了。其实,第一次输出的结果应该是 10,因为编译器会对其有保留作用;而第二次输出的结果是一串乱码,即编译器不会对其进行二次保留了,故得到的结果就错误了。
1.4 堆区及new运算符
(1)由程序员手动分配和手动释放,若程序员不释放,程序结束时由操作系统回收。
(2)new运算符。C++中主要利用new在堆区中开辟空间。释放时利用操作符 delete。
堆区开辟数据的语法: new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
在C++中主要利用new在堆区中开辟空间。如下例:
#include<iostream>
using namespace std;
//1.定义一个函数
int * func()
{
// 利用 new 关键字,可以将数据开辟到堆区
//new int(10); //这里表示用 new 开辟一个整型数据到堆区,初始值为10
int* p=new int(10); //定义一个指针来返回地址编号
return p;
}
int main()
{
//2.接受函数的返回值
int* p = func();
//3.打印结果,测试结果是否正确
cout << "输出*p的结果是:" << *p << endl;
system("pause");
return 0;
}
结果为:
分析:
函数func()中定义的指针 p 其实也是一个局部变量,是存放在栈区中的,而这里利用 new 在堆区中开辟的一个整型数据所占空间,这两个变量是在不同的区上的。在堆区上创建好的数据不是直接将数据直接传出,而是堆区创建数据的地址传出来,用指针p来接受这个数据的地址且输出结果是正确的。这是因为堆区的数据是由程序员来管理的,只要程序员没有将堆区的这个数据清除,这个数据是一直存在的,不像栈区的数据那样是由编译器来进行管理开辟和释放的,也没有所谓的保留之说,所以这样输出的数据也是正确的。
注:在堆区开辟和释放一个变量或一个数组的方式
//用 new 开辟一个整型数据到堆区,初始值为10
int* p = new int(10);
//释放 数据
delete p;
//.在堆区利用new开辟数组
int* arr = new int[10]; //这里代表数组有10个元素,返回值仍然是地址
for(int i = 0; i < 10; i++)
{
arr[i] = i + 1;//给10个元素赋值 1-10
}
for(int j=0;j<10;j++)
{
cout << arr[j] <<" ";
}
//释放堆区数组,要加一个 []才可以
delete [] arr;
二,C++中的引用
1.引用的基本使用
(1)作用:给变量起别名
(2)语法:数据类型 &别名 = 原名
int &b = a; //这里表示 a 的别名是 b
b=10; //则对 b 赋值即是对 a 赋值,它们作用的是同一块内存
2.引用的注意事项
(1)引用必须初始化
(2)引用在初始化后,不可以改变
//1.引用必须初始化
int &b; //错误
//2.引用一旦初始化之后,就不可以更改
int a=10,c=20;
int &b=a; //b 为 a 的别名
int &b=c; //错误。 b 为 a 的别名,不可以再成为其他变量的别名
//3.不能给常量设置引用
int a=10;
int &b=10; //错误。引用必须要引用一块合法的内存空间,可以是栈区或对区上的数据
//而常量是在常量区上的,不可以直接引用。
3.引用做函数参数
(1)作用:函数传参时,可以利用引用的方法让形参修饰实参。
(2)优点:可以简化指针修改实参的过程。
传参的方式有两种:值传递与地址传递。其中,值传递不可以修饰实参,而地址传递可以修饰实参。具体可如下:
#include<iostream>
using namespace std;
//以交换函数作为例子
//1.值传递
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
//2.地址传递
void swap2(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//3.引用传递
void swap3(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 10, b = 20;
cout << "交换前的数据为:" << endl
<< "a=" << a << " " << "b=" << b << endl<<endl;
//1.值传递 形参不会修饰实参
swap1(a, b);
cout << "swap1(值传递) 后的结果为:" << endl;
cout << "a=" << a << endl;
cout << "b=" << b << endl<<endl;
//2.地址传递 形参可以修饰实参
swap2(&a, &b);
cout << "swap2(地址传递) 后的结果为:" << endl;
cout << "a=" << a << endl;
cout << "b=" << b << endl<<endl;
//3.引用传递
swap3(a, b); //这里的 a,b是实参,函数swap3中的 &a,&b相对于分别是这两个实参的别名,
//所以对别名的操作就是相对于对本身的操作
cout << "swap3(引用传递) 后的结果为:" << endl;
cout << "a=" << a << endl;
cout << "b=" << b << endl << endl;
system("pause");
return 0;
}
运行结果如下:
分析:
1.可以看到利用值传递封装交换数值函数来进行数值交换是不可行的,因为它仅仅只是数值上的操作,不可以修饰实参,所以起不到交换作用。
2.利用地址传递方式就可以实现数值交换,因为此时形参和实参此时作用的是同一块内存区域,所以对形参的修改也会导致实参的修改。
3.利用引用传递方式也可以实现交换数值交换,因为交换函数中的形参就是实参的别名,事实上它们也就是作用于同一块内存区域,只是叫不同的名字而已啦。如上,用地址方式将数值变化成:a=20,b=10后,利用引用方式有将数值变回 : a=10,b=10。
4.引用做函数返回值
(1)作用:引用是可以作为函数的返回值存在的。
(2)用法:函数调用可以作为左值。
注意:不要返回局部变量的引用。
通过下面这个例子,应该会比较好理解
#include<iostream>
using namespace std;
//1.不要返回局部变量的引用
int & test1() //函数前加 &,表示要用 引用 的方式返回变量
{ //在这里是 a 来返回值,并且是用引用的方式来返回的。
int a = 10; //局部变量存放在四区中的 栈区,在函数执行完后释放。
return a;
}
//2.函数的调用可以作为左值,即在等号的左边,可以对其进行赋值
int& test2()
{
static int a = 10; //定义一个静态变量,静态变量存放在全局区,全局区上的数据在程序结束之后系统释放。
return a;
}
int main()
{
int & ret1 = test1(); //这里的 ret 就相对于 test1函数中的 a,所以在这里也创建一个
//引用 来接受返回值。
cout << "ret1=" << ret1 << endl; //由于 a 是局部变量,所以执行完后就直接释放了,此时打印出来的是乱码
//如果第一次打印结果是正确的,那么就是编译器做了保留.
int& ret2 = test2();
cout << "ret2=" << ret2 << endl;
//因为这里是在函数前加了 引用,则返回值是返回 a 的一个引用,就相对于把 a 返回了
//那这里 就是相对于:a=100;而这里的 ret2 本身就是 a 的一个别名,所以说 ret2 的值与 a 是一样的
test2() = 100;
cout << "ret2=" << ret2 << endl; //输出为100
system("pause");
return 0;
}
运行结果为:
分析:
以上主要是要理解两点:
1.不要返回局部变量的引用,因为局部变量在其所属的 { } 内的部分执行完之后就会释放,所以返回值就会是一个乱码;
2.函数的调用可以作为左值,即在等号的左边,可以对其进行赋值。在这里需要好好理解,函数前面加上一个 & ,即是使该函数的 返回值 以 引用 的方式来返回,那么在函数中作为返回值的那个变量就是 本身。所以在主函数中定义一个 引用,就是相对于本例中 a 的别名,实质上是相同的。
5.引用的本质
本质:引用的本质在C++内部实现是一个指针常量。这里再解释一下指针常量与常量指针:
指针常量:指针的指向是不可以修改的,而指针指向的值是可以改动的。即 int * const p。
常量指针:指针的指向是可以修改的,而指针指向的值是不可以改动的。即 int const *p。
//发现是引用,会转换为 int *const ref=&a; 指针常量方向不可改,但是所指的值可以修改。
void func(int &ref)
{
ref=100; //ref是引用,转换为 *ref=100;
}
int main()
{
int a=10;
int &ref=a; //自动转换为 int *const ref=&a;指针常量方向不可改,也就是说引用是不可更改的。
ref=20; //内部发现ref是引用,会自动转换为 *ref=20;
cout<<"a="<<a<<endl;
cout<<"ref="<<ref<<endl;
func(a);
retun 0;
}
//我个人是这样理解的,因为引用就是一个变量的别名嘛,而一个变量的地址空间是定义之后就分配好的,
//是不可以随便修改的,而地址空间里的值是可以修改的。所以说,使用的时候不用把 引用 想的太复杂,
//就把它当做是 变量使用就好了,只不过它是别名而已。
结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作,编译器 都帮我们做好了。
6.常量引用
(1)作用:常量引用主要用来修饰形参,防止误操作。
(2)在函数形参列表中,可以加 const 修饰形参,防止形参改变实参。
实例如下:
#include<iostream>
using namespace std;
void showValue(int& val)
{
val = 1000;
cout << "val=" << val << endl;
}
int main()
{
//常量引用
//不能给常量设置引用
int a = 10;
//int& ref = 10; //错误。引用必须要引用一块合法的内存空间,可以是栈区或对区上的数据
//而常量是在常量区上的,不可以直接引用。
//加上 const 之后,编译器将代码修改,相对于int temp=10;const int &ref=temp;
const int& ref = 10; //正确。如果在引用前面加上 const,变成常量引用就没错了。
//ref = 20; //错误。加上const之后就会变为只读,不可修改
int b = 100;
showValue(b); //在val没有改变值时,输出值为 100.
//val值改变之后,b的值也会随之改变
cout << "b=" << b << endl; //那么为了防止不小心将val值修改而导致b也跟着改变,就需要加上const防止误操作。
//即将函数形式改为:void showValue(const int& val)
system("pause");
return 0;
}
运行结果如下:
分析:
由上可以看出,由于引用在函数中的运用,可以带来方便,但同时也会带来误将实参值改变的风险,因为此时形参是可以修饰实参的,所以要尤为地小心。那么就得利用常量修饰符 const ,只要加上 const ,所有的变量都会被修饰为常量,并且为只读状态,不可修改,这样就可以解决误将实参修改的问题了。
二,函数的提高知识
1.函数的参数
1.1 函数的默认参数
在C++中,函数的形象列表中的形象是可以有默认值的。即,在定义函数的时候可以给形参定义一个默认值。
语法:返回值类型 函数名 (参数 = 默认值){ }
1.2 使用默认参数的注意事项
(1)在函数中未定义默认参数时,在调用该函数值,需要传入的参数的个数必须与函数所定义的形参个数相同,否则会出现错误“函数调用中的参数太少”,如下图所示。
(2) 在函数中定义了默认参数之后,在调用该函数时可以不用传入与函数所定义的形参个数相同的参数,如果该函数所有的参数都为默认参数,那么在调用时也可以不传入参数,因为在函数中是有默认参数的,当我们不传入参数时,它会使用自己的默认参数;
当然,如果不想使用默认参数,也可以传入自己想要传入的参数,传入参数后该函数就不会使用自己的默认参数,这样就可以使用我们自己传入的参数值来进行函数调用了。
说明即使是有了默认参数,在调用函数时,我们也还是有选择权和控制权的。
具体实例如下:
运行得到的结果如下:
(3) 如果在函数中定义了默认参数,那么在这个默认参数之后的参数直至形参列表的结尾都需要定义为默认参数,否则会出现错误。当然,只要形参列表的结尾是默认参数,那么前面是不是默认参数是没有要求的。
(4)如果函数声明中有默认参数,那么在函数实现(即函数定义)时就不可以有默认参数了。如下例中,如果偏要这样,就会出现如下错误:
1.3 函数的占位参数
1.3.1 函数的占位参数的含义与语法
(1)在C++中的函数的形参列表里可以有占位参数用来占位,调用函数时必须填补该位置。这样不太好理解,举个日常的例子吧。比如说在学校图书馆里,某位同学想要去吃个午饭,但是呢又想保留住这个位置不被其他人占有,所有就会把自己的一些书本放在位置上,表示这个位置已经有人了。这里的课本就是起着占位的作用。
(2)语法:返回值类型 函数名 (数据类型) { } (只写上数据类型而不写变量名)
1.3.2 使用占位参数的注意事项
(1)虽然在函数中使用占位参数只写数据类型而不用写变量名,但是在调用该函数的时候也需要传入与函数中形参的个数相同的参数个数。
(2)在调用函数时调用的参数个数一定要正确,需要把占位参数的个数也要纳入其中,但是传入的与占位参数对应的参数值是传不到占位参数那里的,也就是说占位参数是接收不到传入的参数的。所以这种占位参数比较少用,但是要了解这样一种参数,在某些情况下也是有用处的。
(3)占位参数还可以包纳默认参数。 此时调用该函数时,可以不用传入与函数形参个数相同的参数个数,因为有默认函数的存在嘛。看到这里,可能都会觉得这个占位参数实用性真的不高,感觉学起来也没什么用,但是知识肯定是有用的嘛,说不定继续往后面学就可以了解到它的用途了呢。
如下图所示:
2.函数的重载
2.1 函数重载的作用及满足条件
(1)作用:允许函数名可以相同,大大提高函数的复用性。
(2)函数重载满足的条件:
a . 在同一个作用域下;
b. 函数名称相同;
c. 函数参数 类型不同 或者 个数不同 或者 顺序不同。
接下来通过下面这一个例子来理解上面这些知识点:
从运行结果上可以看到,这是错误的写法。错误的点就需要我们从函数重载的三个满足条件来分析原因:
(1)首先,这两个函数的作用域要相同,它们的函数作用域都在全局作用域下,所以这是满足的;
(2)第二,函数名称是相同的,显而易见;
(3)第三,要满足函数参数 类型不同 或者 个数不同 或者 顺序不同,从上面代码可以看到这两个函数都是没有参数的,所以这个条件是不满足的。
如果满足第三个条件呢?通过改变参数来满足重载函数的条件,下面分别通过对参数的个数,类型,顺序来举例说明。 如下例,可以先思考以下代码,当调用函数时,究竟调用的是哪一个函数呢?
a. 通过改变函数参数 个数 的不同:
没错,就是第一个啦。从上面的运行结果看到,调用的是第一个函数,因为调用函数时传入的参数是与第一个函数相对应的(从参数个数来判断)。下面的例子也是同理的哟,可以自己多多思考,认真理解以下。
b. 通过改变函数参数 类型 的不同
c. 通过改变函数参数 顺序 的不同
2.2 函数重载的注意事项
(1)函数的返回值不可以作为函数重载的条件
分析:
因为在调用函数时,这两个函数都可以被调用进去,但是此时就会产生歧义,编译器不知道究竟要调用哪一个就会报错。所以一定要记住函数的返回值是不可以作为函数的重载条件的。
(2)引用作为函数重载条件
仔细看看这段代码,小伙伴们可以先思考,这里在调用函数时 ,调用的是哪一个呢?
结果分析:
因为这里的两个函数的作用域相同,函数名相同,并且参数类型是不同的,满足函数重载的三个条件。在主函数中定义的是变量,在第一个函数中的参数是一个变量,而第二个函数的参数由于const的修饰变成了一个常量(变量是可读可写的,而常量是只读的),所以说在调用这个函数时就会调用第一个函数喽。
再来,如果是这样的呢?
结果分析:
这里在调用函数时,直接以一个常数作为参数传入,那么所对应的参数类型就是常量,当然也就会调用第二个函数喽;另外,const int &a=10,表示是经过const的修饰,会对代码起到优化的作用,相当于编译器会创建一个临时变量,让这个a 指向这个临时空间。对于第一个函数的参数的分析,这是一个引用作为函数参数,而 int &a=10,因为这里的引用a是作为一个变量的别名而存在的,这里是明显不合法的,所以说一定不会调用第一个函数的。
(3)函数重载与默认参数
当函数重载与默认参数在一块儿的时候会发生特殊情况,如下例:
结果分析:
1. 可以看到,这种重载函数与默认参数在一块是会容易出错的。在这里调用函数时,如果是传入两个参数,那么就会调用第一个函数,这是毫无疑问的。
2. 但是这里如果是如上例,只传入一个参数呢?情况就不一样了。由于第一个func函数的第二个参数是默认参数,所以在调用函数时,只需要传入一个参数就可以调用这个函数了,显然这里是可以调用的;而第二个函数也是显然可以被调用的,这样就把编译器整不会了,不知道到底要调用哪一个,就会出错。所以说,我们要尽量避免这种情况的发生,在使用函数重载的时候就尽量不要使用默认参数了。
三,C++中的类和对象
1.对于类和对象的理解
1.1 什么是类和对象
(1)在C++这门面向对象的这门语言中,有三大重要的特性:封装,继承,多态。
(2)在C++中,一切事物都可以作为对象,每个对象都有它自己的属性和行为。如人就可以作为对象,人的属性就有姓名,年龄,性别,身高,性格等,行为有走路,跑步,睡觉等。
(3)那么根据这些对象的属性和行为,我们就可以划定为:具有相同性质的对象,就是属于一个类。最通俗易懂的就是“人类”了,因为我们人都有相同的属性嘛,即使每个人的属性值不同,但是依然也是属于一个类中的。
2.特性一 ——封装
2.1 封装的意义及权限
(1)将对象的属性和行为作为一个整体来表现生活中的事物。即,在设计类时,我们把属性和行为写在一起来表现一个事物。
语法:class 类名 { 访问权限: 属性 / 行为 } ;
一些关于叫法的问题:
类中的属性和行为 称为类的 成员;
属性 成员属性 成员变量
行为 成员函数 成员方法
话不多说,都在代码里,接下来看着两个例子。这两个例子主要目的是要我们熟悉设计类的语法与方式,以及创建对象(实例化)的过程, 对对象赋值的两种方式:通过对象属性直接赋值 或 通过对对象的行为来对其属性赋值。
例1:设计一个圆类,求圆的周长和面积
#include<iostream>
using namespace std;
//设计一个圆类,求圆的周长与面积
//首先要先到,求圆的周长与面积需要用到哪些条件,也就是圆身上的哪些属性。
//从求解公式上思考:求周长 C = 2 * π * R ; 求面积 S = π* R^2
//显然,这里我们就要用到圆身上的 半径 属性,所以我们在设计类的时候就可以把 半径 作为属性在内了。
// 开始设计圆类 语法:class 类名 { 访问权限: 属性 / 行为 } ;
const double PI = 3.14; //先把 圆周率 定义为一个常量,圆周率是不变的。
class Circle
{
//访问权限
public: //公共权限
//属性 一般都是定义一个变量来表示这个属性
int R; //半径
//行为 一般都是定义一个函数来表现这个行为
double calculate_ZC() //获取圆的周长
{
return 2 * PI * R;
}
double calculate_MJ() //获取圆的面积
{
return PI * R * R;
}
};
int main()
{
//通过对类的创建完成之后,才可以具体创建一个对象
//通过 圆类 来创建一个具体的对象(圆),这个 过程 也叫做 实例化。
Circle C1;
//给创建好的 对象 的 属性 进行赋值
C1.R = 4;
cout << "半径为 " << C1.R << " 的圆的半径为:" << C1.calculate_ZC() << endl
<<" "<<"面积为:"<<C1.calculate_MJ()<<endl;
cout << endl<< endl;
system("pause");
return 0;
}
例2:设计一个学生类,方法与例1相同。例1是以对象的属性来进行赋值,这里主要是介绍利用对象的行为来对其属性进行赋值的方法。如下:
(2)将属性和行为加以权限控制。
在设计类时,可以根据需要,把属性和行为分别放在不同的权限下面来加以管理与控制。
1.三种访问权限:
a. public 公共权限 成员 在类内可以访问 类外也可以访问
b. protected 保护权限 成员 在类内可以访问 类外不可以访问
c. private 私有权限 成员 在类内可以访问 类外不可以访问
代码是最好的解释方式,如下:
2.2 C++中class与struct的区别
在学习了类之后,我们发现它是与结构体比较相似的,它们都有自己的成员属性。在C++中,struct与class的唯一区别就在于 默认的访问权限 不同,在class中可以根据需要设置不同的权限,而在struct中则不可以。在class中 ,默认的访问权限是 私有权限;而在struct中,默认的访问权限是公有权限。
如下例:
2.3 成员属性私有化
将所有成员属性设置为私有的好处:
a. 公有权限是全局都有可读可写的权限的,而设置为私有权限可以自己控制读写权限。
如,我们可以自己设置某个成员属性为:可读可写 或 只读 或 只写。
b. 对于写权限,我们可以检测数据的有效性。
如下例:
#include<iostream>
#include<string>
using namespace std;
//设计学生类
class Student
{
//访问权限
public:
void setname(string name) { //可写。赋值姓名行为
m_name = name;
}
string getname() //可读。尤其注意这里 要使返回值类型与函数类型匹配。
{
cout << "姓名为:" <<m_name<< endl;
return m_name;
}
void setage(int age){ //可写。年龄范围规定在 1--150
//判断年龄的有效性
if (age <= 0 || age > 150)
{
cout << "请输入有效正确的年龄,年龄范围规定在 1--150" << endl;
}
else
{
m_age = age;
}
}
int getage()
{
cout << "年龄为:" << m_age << endl;
return m_age;
}
int getID() //只读
{
cout << "ID为:" << m_id << endl << endl;;
return m_id;
}
void setpassword(int password)
{
m_password = password;
cout << "这里输出密码仅仅用来判断是否将数据写入,而不是代表它有可读性" << endl;
cout << "密码为:" << m_password << endl;
}
private:
string m_name; //设置为 可读可写
int m_age; //设置为 可读可写
int m_id=123; //设置为 只读
int m_password; //设置为 只写
};
int main()
{
//实例化
Student stu1;
//姓名 可读可写
stu1.setname("李四");
stu1.getname();
//年龄 可读可写
stu1.setage(12);
stu1.getage();
//学号 只读
stu1.getID();
//stu1.m_id = 12; //报错。只读权限,不能对其赋值。
//密码 只写
stu1.setpassword(1234);
system("pause");
return 0;
}
当我们设置好权限之后,就不可以越界访问了。如下:
3.对象的特性
3.1 构造函数和析构函数
(1) C++利用构造函数和析构函数解决:一个对象或者变量没有初始状态,对其使用后果是未知的安全问题;使用完一个对象或者变量没有及时清理所带来的安全问题。这两个函数将会被编译器自动调用,完成对象的初始化和清理工作。对象的初始化和清理工作是编译器是强制我们要做的事情,所以我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数,是空实现。
(2)构造函数:主要作用于创建对象时,为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
(3)析构函数:主要作用于对象销毁前系统自动调用,执行清理一些工作。
(4)构造函数语法: 类名(){ }
4.1 构造函数,没有返回值也不写void。
4.2 函数名称与类名相同。
4.3 构造函数可以有参数,所以可以发生重载;也可以不写参数。
4.4 程序在调用对象的时候会自动调用构造,无需手动调用,而且只会调动一次。
(5)析构函数语法:~类名(){ }
5.1 析构函数,没有返回值也不写void。
5.2 函数名称与类名相同,在名称面前加上符号。
5.3 析构函数不能有参数,因此不能发生重载。
5.4 程序会在对象销毁前自动调用析构,无需手动调用,而且只会调动一次。
如下例:
#include<iostream>
using namespace std;
//对象的初始化和清理
//1.构造函数 进行初始化操作
class Person
{
public:
//1.1 构造函数
/*1.1.1构造函数,没有返回值也不写void。
1.1.2 函数名称与类名相同。
1.1.3 构造函数可以有参数,所以可以发生重载;也可以不写参数。
1.1.4 程序在调用对象的时候会自动调用构造,无需手动调用,而且只会调动一次。*/
Person()
{
cout << "Person 构造函数的调用:" << endl;
}
/*析构函数语法:~类名(){ }
5.1 析构函数,没有返回值也不写void。
5.2 函数名称与类名相同,在名称面前加上符号。
5.3 析构函数不能有参数,因此不能发生重载。
5.4 程序会在对象销毁前自动调用析构,无需手动调用,而且只会调动一次。*/
~Person()
{
cout << "析构函数的调用" << endl;
}
};
//创建一个对象
//析构和构造都是必须要有的实现,如果我们自己不提供这两个函数,编译器会提供一个空实现的构造和析构。
void creat_per()
{
Person pe1; //在栈上的数据,text1执行完毕后,释放这个对象。
}
int main()
{
//运行之后发现,只是创建了一个对象就可以对构造函数和析构进行调用了。
cout << "在全局函数中创建对象" << endl;
creat_per();
cout << endl;
//如果在main函数中创建一个对象
cout << "在main函数中创建一个对象:" << endl;
Person per2;
system("pause");
return 0;
}
结果分析:
(1)可以看到,在全局函数中创建对象和在main函数中创建对象所运行出来的效果是不一样的,在全局函数中创建一个对象,而后在main函数中调用,运行结果会显示调用了构造和析构;但是在main函数中创建一个对象,运行结果显示只是调用了构造。这是为什么呢?
(2)因为在全局函数中创造一个对象,当函数调用执行完毕之后就会被释放,但是它是在main函数中的程序语句执行完之前执行完的,所以我们可以在窗口看见运行结果上显示调用了构造和析构;
(3)但是在main函数中创造对象时,执行完 “Person per2”语句之 后,就顺序运行到下一行了,即system("pause"),程序就暂时中断到这里了,当我们按下任意键之后,执行到return 0之后才算main函数执行完毕了。这时候,在main函数中创建的对象才会开始释放,但是由于main函数执行结束,此时窗口已经不见了,所以再析构的时候我们也就没有窗口去看见它显示了。
3.2 构造函数的分类和调用
(1)两种分类方式:
1.按参数分为:有参构造和无参构造。
2.按类型分为:普通构造和拷贝构造。
(2)三种调用方式:
1.括号法
2.显示法
3.隐式转换法
#include<iostream>
using namespace std;
//1.构造函数的分类及调用
//分类
/*按照参数分类 无参构造(默认构造)和 有参构造
按照按类型分为 普通构造 和 拷贝构造(相对于把一个对象的属性全部复制到另外一对象身上)。
*/
class Person
{
public:
int age;
Person() //无参构造函数
{
cout << "Person 无参构造函数的调用" << endl;
}
Person(int a) //有参构造函数
{
age = a;
cout << "Person 有参构造函数的调用" << endl;
}
//拷贝函数
Person(const Person &per1) //加一个const,不能把本体也给覆盖掉,用引用方式传入进来
{
age = per1.age; //把per1的年龄信息复制过来。
cout << "Person 拷贝函数的调用" << endl;
}
~Person()
{
cout << "Person 析构函数的调用" << endl;
}
};
//调用
void text1()
{
//1.括号法;
cout << "括号法:" << endl;
Person per1; //默认构造函数调用,即无参构造函数的调用。创建一个对象就会自动调用。
Person per2(10); //有参函数的调用
Person per3(per2); //拷贝函数的调用
//注意事项1:
//在调用默认构造函数时候,不要加 (),如写成 Person per1()
//由于该语句与函数声明的语法类似,编译器会认为这是一个函数声明,不会认为是在创建对象。
cout << "Per2的年龄为:" << per2.age << endl;
cout << "Per3的年龄为:" << per3.age << endl << endl;
//2.显示法; 先创建对象名再赋值
cout << "显示法:" << endl;
Person per4;
Person per5 = Person(20); //调用有参构造函数
Person per6 = Person(per5); //调用拷贝构造函数
cout << "Per5的年龄为:" << per2.age << endl;
cout << "Per6的年龄为:" << per3.age << endl << endl;
/*注意事项1:
对于 Person(20),单独写出来时,是一个匿名对象
特点:当前行语句执行完之后,系统会立即回收匿名对象
下面可以做一个测试:
*/
Person(20);
cout << "匿名对象" << endl;
/*注意事项2:
不要利用拷贝构造函数 来 初始化匿名对象。
对于 Person(per2),编译器会认为 Person(per5) 是对象的声明 等价于 Person per5*/
// Person(per5);
//3.隐式转换法。
Person per7 = 20; //相对于 Person per5 = Person(20); //调用有参构造函数
Person per8 = per7; //调用拷贝构造函数
}
int main()
{
//1.括号法;
text1();
system("pause");
return 0;
}
此运行结果为以下代码的结果:
/*注意事项1:
对于 Person(20),单独写出来时,是一个匿名对象
特点:当前行语句执行完之后,系统会立即回收匿名对象
下面可以做一个测试:
*/
Person(20);
cout << "匿名对象" << endl;从运行结果可以看出,打印结果在析构函数之后出现,所以说匿名对象所在行语句执行完成之后会立即被回收,即会立即调用析构函数进行清理释放。
3.3 拷贝构造函数的调用时机
C++中拷贝函数调用时机通常有三种情况:
(1)使用一个已经创建完毕的对象来初始化一个新对象
(2)值传递的方式给函数参数传值。
(3)以值方式返回局部对象。
三种方式具体用法见以下代码:
#include<iostream>
using namespace std;
//创建一个类
class Person
{
public:
int age;
Person() //构造函数
{
cout << "Person 默认构造函数的调用" << endl;
}
Person(int a) //有参构造函数
{
age = a;
cout << "Person 有参构造函数的调用:" << endl;
}
//拷贝函数
Person(const Person& per1) //加一个const,不能把本体也给覆盖掉,用引用方式传入进来
{
age = per1.age; //把per1的年龄信息复制过来。
cout << "Person 拷贝函数的调用:" << endl;
}
~Person() //析构函数
{
cout << "Person 析构函数的调用:" << endl;
}
};
//拷贝构造函数调用时机
//1.使用一个已经创建完毕的对象来初始化一个新对象
void text1()
{
Person per1(10); //有参函数的调用
Person per2(per1); //拷贝函数的调用
cout << "per2 的年龄为:" << per2.age<<endl;
}
//2.值传递的方式给函数参数传值 值传递不会修改本体
void dowork1(Person per2) //以对象作为值来当做参数
{
}
void text2()
{
Person per2; //默认构造函数调用
dowork1(per2); //拷贝构造函数调用
}
//3.以值方式返回局部对象
Person dowork2() //以对象作为返回值来返回
{
Person per3; //默认构造函数调用
cout <<"dowork2()中的per3的地址是:" << (int*)&per3 << endl;
return per3; //返回的是 对象
}
void text3()
{
Person per3 = dowork2(); // 拷贝函数调用
cout<<"text3()中per3 的地址是:" << (int*)&per3 << endl;
//打印出地址, 运行结果是不一样的,说明这两个per3是不一样的。
}
int main()
{
cout << "使用一个已经创建完毕的对象来初始化一个新对象" << endl;
text1();
cout << endl;
cout << "值传递的方式给函数参数传值 " << endl;
text2();
cout << endl;
cout << "以值方式返回局部对象" << endl;
text3();
system("pause");
return 0;
}
3.4 构造函数调用规则
(1)默认情况下,只要创建了一个类,C++编译器至少给一个类添加3个函数
1. 默认构造函数(无参,函数体为空,即空实现)。
2.默认析构函数(无参,函数体为空,即空实现)。
3.默认拷贝构造函数,对属性进行值拷贝,相当于直接用一个数值对其进行赋值操作。
(2)构造函数调用规则如下:
1.如果用户定义有参构造函数,C++不再提供无参默认构造函数了,但是会提供默认拷贝构造函数。
2.如果用户定义拷贝构造函数,C++不会再提供其他构造函数。
1.这一段代码是写好了拷贝构造函数的,运行结果与我们所预料的相同,这里不再或多阐述,把这个例子拿出来只是用来作为比较。
如下:
接下来这段代码是把拷贝构造函数注释了的,运行之后思考运行结果 :
结果分析:
可以看到运行结果中只有默认构造函数被调用了,但是没有拷贝构造函数的调用per2的年龄值却被正确输出了,并且只有一个函数的调用却有两个析构函数的调用。再与第一段代码的输出结果做一下比较,其实这里就可以证明:默认情况下,只要我们创建了一个类,C++编译器会默认给我们三个函数, 默认构造函数,默认析构函数,默认拷贝构造函数。
再根据调用构造函数的两个规则,我们进行以下代码的调试运行:
1. 1 注释掉默认函数:
1.2. 基于1,我们将调用默认构造函数的语句注释掉,利用有参构造函数创建一个对象,并且注释掉拷贝构造函数:
运行结果分析:
1.1 可以看到例1.1中的运行结果发生错误,这是因为在类中我们没有去定义一个默认构造函数,只定义了有参构造函数和拷贝构造函数,也正是由于有参构造的存在,编译器也不会再提供默认构造了,那么我们再调用默认构造函数,必然会是发生错误的。
1.2 从运行结果可以发现,即使我们注释掉了拷贝函数,但是仍然可以调用拷贝函数创建对象。这说明,如果我们定义了一个有参构造,即使我们自己没有定义拷贝构造,编译器也会自动帮我们构造一个,有了拷贝构造函数,就得有一个析构函数进行清理释放工作,所以在运行结果中我们可以看到析构函数被调用了两次。
以上印证了构造函数调用的第一条规则。即,如果用户定义有参构造函数,C++不再提供无参默认构造函数了,但是会提供默认拷贝构造函数。
2.在上述的代码中把除了拷贝构造函数之外的构造函数都注释掉
运行结果分析:
通过运行结果我们可以看到是报错的,显示不存在默认构造函数以及没有与参数列表匹配的构造函数,则说明,如果我们 定义了拷贝构造函数,C++就不会再提供其他构造函数。
3.5 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作。
深拷贝:在堆区重新申请空间,进行拷贝操作。
3.5.1.浅拷贝的使用及其不足
运行结果分析:
(1)在未使用深拷贝的方法时,执行代码后会导致程序崩溃。这是因为由于是浅拷贝,只是将值简单的赋值拷贝,其地址也会一同复制过去,此时就使得这两个对象都指向同一块内存地址空间,而在对象所在的代码执行完之后就得释放,那么根据先进后出的原则,最先被释放的就是per2(通过析构函数释放),然后再释放per1(通过析构函数的if语句中的delete来释放堆区的空间),但这两个对象所指向的地址都是一样的,所以就相当于对这块内存进行了两次释放,这是不合法的。
(2)由上我们还可以发现,如果我们是在栈区开辟的空间那么就不会出现内存重复释放的问题;如果是在堆区开辟空间,那么才会出现这些问题,所以在我们平时开发中就要想清楚这些问题,在堆区开辟就要使用相关的办法去解决。
代码见以下,是为定义深拷贝函数之前的代码哟,由1表示的。
3.5.2 利用深拷贝操作, 解决浅拷贝带来的问题
(1) 通过对以上浅拷贝所带来的问题的分析,我们可以自己实现定义拷贝构造函数,解决浅拷贝带来的问题 ,即 让调用拷贝函数的对象类外开辟一个空间,这样就不会公用同一块地址而导致内存重复释放了。
(2) 我们先看看运行结果,检测一下实际效果:可以看到运行结果中,有两个析构函数调用了,这就说明两个对象分别调用的是自己的析构函数,而不是交叉的重复释放了。
(3) 在一般情况下,我们可以不写析构函数,编译器也会给我们提供,但是有在堆区开辟的数据,就必须得写析构函数了,通过析构函数可以可以将堆区开辟的数据做释放操作。
具体代码如下,注释中带有我自己理解的一些知识点。
#include<iostream>
using namespace std;
//创建类
class Person
{
public:
int m_age;
int* m_height; //定义一个指针变量,目的是接收开辟在堆上的变量的值。
Person()
{
cout << "Person默认构造函数的调用:" << endl;
}
Person(int age,int height)
{
m_age = age;
m_height =new int(height); //将变量开辟在堆区.堆区的数据需要程序员手动开辟,也需要程序员手动释放。
//什么时候释放?就是在m_height执行完之后就可以释放了。而在执行完之后,
//都会调用到析构函数,即在调用析构函数的时候就表示某个构造函数执行完了。
cout << "Person的有参构造函数的调用" << endl;
}
//2. 自己实现定义拷贝构造函数,解决浅拷贝带来的问题。
Person(const Person& per1)
{
cout << "Person 的拷贝构造函数的调用" << endl;
m_age = per1.m_age;
// m_height = per1.m_height; //当我们不写拷贝构造函数时,编译器默认写下的就是这行代码,但是由于我们要解决
//在堆区上开辟数据并且在浅拷贝中出现的 内存地址重复释放的问题,即指针(地址)的问题,所以先注释掉这行代码
//深拷贝操作 即 让调用拷贝函数的对象类外开辟一个空间,这样就不会公用同一块地址而导致内存重复释放了。
m_height= new int(*per1.m_height);
}
~Person()
{
//析构函数对于堆区开辟的数据的用途
//析构代码部分,可以将堆区开辟的数据做释放操作。
if (m_height != NULL) //因为在用 new 开辟空间如果不成功的话就会返回 NULL,所以可以用此作为判断
{
delete m_height;
m_height =NULL;
}
cout << "析构函数的调用" << endl;
}
};
//测试
void text1()
{
Person per1(20,165);
cout << "per1的年龄为:" << per1.m_age <<" "<<"身高为:"<<*per1.m_height<< endl;
//1.浅拷贝 简单的赋值拷贝
Person per2(per1); //输出结果后发现,即使我们没有定义拷贝函数,编译器也帮我们做好了拷贝函数做的事情
//即,成功把这里的 per1 的值 复制 给了 per2 对应的属性的值。 这就是浅拷贝,简单的赋值拷贝。
cout << "per2的年龄为:" << per2.m_age<<" " << "身高为:" << *per2.m_height<< endl;
}
int main()
{
//调用
text1();
system("pause");
return 0;
}
3.6 初始化列表
(1) 作用:C++提供了初始化列表语法,用来初始化列表属性。
(2) 语法:构造函数(): 属性1(值1),属性2(值2)....{ }
具体例子如下:
#include<iostream>
using namespace std;
//初始化类
class Person
{
public:
int m_age;
int m_height;
int m_weight;
//传统的初始化操作 定义构造函数的同时给属性赋值
/*Person(int age, int height, int weight)
{
m_age = age;
m_height = height;
m_weight = weight;
}*/
//利用初始化列表来初始化属性
//语法:构造函数(): 属性1(值1),属性2(值2)....{ }
Person(int a, int b, int c) : m_age(a), m_height(b), m_weight(c)
{
}
};
//测试
void text1()
{
/*Person per1(12,145,65);*/
Person per1(12,354,56);
cout << "per1 年龄为:" << per1.m_age << endl;
cout << "身高为:" << per1.m_height << endl;
cout << "体重为:" << per1.m_weight << endl;
}
int main()
{
//调用
text1();
system("pause");
return 0;
}
3.7 类对象作为类成员
C++类中的成员可以是另外一个类的对象,我们称该成员为 对象成员。
比如说,有一个对象A作为B类中的成员,则A为对象成员。那么,当创建B的对象的时候,类A与B的构造和析构函数的调用顺序是什么呢?
#include<iostream>
#include<string>
using namespace std;
//创建手机类
class Phone
{
public:
Phone(string name) //构造函数
{
cout << "Phone 的构造函数调用" << endl;
m_pname = name;
}
~Phone()
{
cout << "Phone 析构函数的调用" << endl;
}
string m_pname;
};
//创建人类
class Person
{
public:
Person(string name, string pName) : m_name(name), m_phone(pName)
{
cout << "Person 构造函数的调用" << endl;
}
//姓名
string m_name;
//手机
Phone m_phone;
~Person()
{
cout << "Person 析构函数的调用" << endl;
}
};
//当其他类的对象作为本类的成员时,构造时先构造类对象,再构造自身。
//析构时,先释放 自身类,再释放 对象成员的类。
//测试
void text1()
{
Person per1("张三","Aphone");
cout << per1.m_name << "拿的手机是:" << per1.m_phone.m_pname << endl;
}
int main()
{
//调用
text1();
system("pause");
return 0;
}
运行结果分析:
根据运行结果显示,Phone 的构造函数比 Person 的构造函数先调用;Person 的析构函数比 Phone 的析构函数先调用。相对于栈的操作,先进后出。总结如下:
(1)当其他类的对象作为本类的成员时,构造时先构造类对象,再构造自身。
(2)析构时,先释放 自身类,再释放 对象成员的类。
3.8 静态成员
(1)静态成员就是在成员变量和成员函数前加上关键字static,即为静态成员。
(2)静态成员分为:
1.静态成员变量:
a.所有对象共享同一份数据;
b.在编译阶段分配内存;
c.类内声明,类外初始化(必须要有一个初始值)。
2.静态成员函数:
a.所有对象共享同一个函数;
b.静态成员函数只能访问静态成员变量。
(3)静态成员的调用方式:
1. 通过对象访问。
2. 通过类访问。
接下来通过几个例子来说明这几个特点:
3.8.1 静态成员变量
1. 类内对成员进行声明,但不对其进行类外初始化 :
运行结果分析:
可以看到,虽然控制台显示“未找到相关问题”,但是编译运行时会出现“无法解析外部命令”的错误,事实上这种错误是在链接阶段才会出现的错误,编译器会默认该变量所对应的值已经存在了,但去寻找的时候又找不到这个值, 那么就会报错。这就说明只有在静态成员初始化之后,才能够去访问所对应的那块内存,也就是说类外初始化是非常必要的。
所以完整的创建静态成员的过程应该是如下这样,一定要类外初始化:
2. 在1.的代码基础上,根据 “所有对象共享同一份数据”来进行测试。再创建一个对象,并对其对象的属性值进行修改,观察原来的对象,即创建的第一个对象所对应的属性值是多少。
(1) 运行结果分析:
当我们对per2的属性值进行修改之后,per1的对于的该属性值也发生了同样的变化。这就说明对于静态成员变量,它不属于某个对象,而是所有的对象都共享其同一份数据。由于静态成员不属于某个变量,所以我们也可以通过类去直接访问静态变量,即可以不用先创建对象再去访问成员。
(2)由上我们的静态成员就可以有两种访问方式:
1.通过对象进行访问
2.通过类名进行访问
如下示例:
3.静态成员变量也是与访问权限的,如例在类中声明一个静态成员在private权限中,并进行类外初始化,那么 能否被成功调用呢?
运行结果分析:
可以看到编译器显示“不可访问”错误,这是因为,即使静态变量是在类外初始化的,但是它是在类内的“private”下声明的,类外是访问不到私有静态成员变量的。
3.8.2 静态成员函数
根据静态成员函数的两个特点,我们对其进行验证说明,如下代码及其注释:
1.先附上完整代码:
#include<iostream>
using namespace std;
//1.静态成员函数:
//
//a.所有对象共享同一个函数;
//
//b.静态成员函数只能访问静态成员变量。
class Person
{
public:
//定义静态成员函数
static void func()
{
m_age = 12; //静态成员函数 可以访问 静态成员变量
//m_weight = 18; //静态成员函数 不可以访问 非静态成员变量
cout << "static void func 的调用" << endl;
}
static int m_age; //定义静态成员变量
int m_weight; //定义一个非静态成员变量
//静态成员函数也是有访问权限的
private:
static void fun2()
{
cout << "static void fun2() 的调用" << endl;
}
};
//对静态成员变量进行类外初始化
int Person::m_age = 10; //也可以是: int age=10;
void text1()
{
//1.通过对象访问
Person per1;
per1.func();
//2.通过类访问 可以通过类访问静态成员函数,说明与它对象无关,所有的对象都是共享一个静态成员函数的。
Person::func();
//3.调用 private 的静态成员函数
//Person::fun2(); //在类外访问不到私有静态成员变量
}
int main()
{
text1();
system("pause");
return 0;
}
2. 静态成员函数只能访问静态成员变量
3.静态成员函数也有访问权限
运行结果分析:
(1)首先,可以通过类来访问静态成员函数,那就说明所有的对象都是共享静态成员函数的,它不属于某一个对象,所以不用特别创建一个对象来对其进行访问。
(2)在用静态成员函数去访问非静态成员变量时,会报错“非静态成员引用必须与特定对象相对”。这是因为非静态成员变量是要区分特定对象的,也就是我们要访问它的时候,必须要创建一个对象来进行访问。而我们用静态成员函数对其访问时,由于静态成员函数没有区分特定对象的能力,即无法区分到底是哪个对象的成员变量,所以是不能用静态成员函数来访问非静态成员变量。静态成员函数和静态成员变量都是程序中独有的共享的一份数据,所以是不区分对象的,类和对象都可以访问这份数据,故静态成员函数所访问的数据也必定是要唯一独有的,所以静态成员函数只能访问静态变量。
(3)静态成员函数也是有访问权限的,与静态成员变量类似,类外是访问不到私有静态成员函数的。
3.9 成员变量和成员函数的分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
1.空对象占用的内存空间 (1个字节 )
2.定义一个非静态成员变量后,对象所占用的空间 (4个字节)
3.在2的基础上,添加一个静态成员变量后类对象占用的内存空间 (4个字节)
4. 在以上基础上,加入静态成员函数与非静态成员函数后的类对象的占用空间 (4个字节)
测试结果分析:
由上可以得到结论:在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
3.10 this指针的用途
3.10.1 this指针的概念
(1)由上面的学习我们可以知道在C++中的成员变量和成员函数时分开存储的。每一个非静态成员函数只会产生一份函数实例,也就是说是多个同类型的对象会共用一块代码。
(2)那么问题就来了,这一块代码是如何区分究竟是哪个对象调用的自己呢?
答案是:C++通过提供特殊的对象指针,this指针,解决上述问题,this指针指向被调用的成员函数所属的对象,这样就可以知道是哪个对象调用的成员函数了。
(3)this不需要定义,可以直接使用。
3.10.2 this指针的用途
(1)this指针是隐含在每一个非静态成员函数内的一种指针,解决名称冲突,分清对象。
(2)在类的非静态成员函数中返回对象本身,可使用 return *this。
1.解决名称冲突
运行结果分析:
从结果中可以看到,如果我们把成员变量和形参变量名取为相同的,那么就会导致编译器认为这两个是一样的,就不会完成对成员属性的复制。
解决方式:
(1)将其名改为不相同的,一般在成员变量名前面加一个 m_xxx,表示是一个成员变量。
(2)利用this指针,来指向当前在调用函数的对象的属性。
如下:
2.返回对象本身
以上是简单的对象调用一次成员函数的效果,那如果一个对象需要多次调用呢?由于函数类型为void,所以我们直接多次使用就会报错,因为它没有返回值,所以就认为没有对象再调用方法,所以我们在多次调用成员函数时,需要这个成员函数的返回值是对象本体,这样每次调用完该函数返回的都是这个对象,就可以实现一个对象调用成员函数多次了,然后我们就可以利用this指针来返回当前调用函数的对象,然后一直调用就可以了呀,这种调用多次的调用方式叫做链式编程思想。注意:这里要返回对象本体,那么函数的类型就得是:Person& 的类型,即利用引用的方式来返回对象本体。在上面1的基础上,写如下代码:
如果不是以引用的方式来返回对象本体,而是以值的方式来返回呢?如下:
运行结果分析:
结果发现,结果不再是上面1那样一次性多次调用函数并且得到正确结果了,而是只调用了一次,值为28。这是因为,当per2第一次调用了这个函数之后返回的不再是对象的本体了,每次调用完都会创建处一个新的对象来对该函数进行调用。还记得拷贝函数的调用时机的知识点吗?当值传递的方式给函数参数传值或返回值时,会调用拷贝构造函数从而复制处一份新的数据出来作为了返回值,也就是说,返回值已经不再是我们第一次调用函数的那个对象本体了,所以本体只会调用一次函数就不再是它本体调用了,也就不会实现一个对象多次调用了。
3.11 空指针访问成员函数
在C++中,空指针也是可以调用成员函数的,但是也要注意有没有用到this指针。
如果有到了this指针。需要加以判断保证代码的健壮性。
#include<iostream>
using namespace std;
class Person
{
public:
void showClassName()
{
cout << "this is a person class" << endl;
}
void showage()
{
cout << "age=" <<m_age<< endl;
}
int m_age;
};
//测试
void text1()
{
//创建一个空指针
Person* p = NULL;
//访问成员函数
p->showClassName();
p->showage();
}
int main()
{
text1();
system("pause");
return 0;
}
以上这段代码运行时出现崩溃,终端界面会闪退,说明代码程序出错了。读取访问权限冲突
先注释掉 p->showage() 这行代码,如下,可以看到代码正常运行了。
再试着注释掉 p->showClassName() 这行代码,如下:
运行结果是终端界面闪退,是处于崩溃状态,所以说这行代码是有问题的。
空指针读取访问权限冲突。
结果分析:
第二个函数中,访问了成员属性,每个成员属性前面都有一个默认的this指针,表示当前对象的属性,即 this->m_age。
而这里并没有创建实体对象,没有确切的指向一个对象,就根本不会访问到数据。报错的原因是因为传入的指针是空指针NULL(无中生有还要访问属性那肯定是错误的啦)。
那么平时开发过程中,为了防止传入空指针而导致程序崩溃,在函数函数中可以加上这样几行代码:
if (this==NULL)
{
return;
}
如下可以看到,程序不再崩溃了。
3.12 const 修饰成员函数
(1)常函数:
1.成员函数后加const,称之为常函数。
2.常函数内不可以修改成员属性。
3.只有const修饰时,为只读状态。但是在成员属性声明时加关键字mutable后,在常函数中依然可以修改。
(2)常对象:
1.声明对象前加const,称之为常对象。
2.常对象只能调用常函数。
3.12.1 常函数
结果分析:
(1)每个成员函数都有默认的this指针,如果是在普通的成员函数(即先不用 const修饰时)中,这样写是没错的,this指针的本质是 指针常量(相当于 Person * const this),const修饰的是指针的指向(地址),所以指针的指向是不能修改的,指向的内容可以改变。
(2)这里再复习一下 常量指针,const int *p 或int const *p,const修饰的是指针指向的内容,表示指针的指向可以改变,但是指针指向的内容不可以修改。
(3)在成员函数后面加上const之后就变成了一个常函数 ,这时候就相当于一个 指针常量+常量指针 的状态了,即 const Person * const this,指向和值都被修饰成了一个常量,此时是不能修改指针的指向和其所指向的值的,所以当我们为其赋值的时候就会报此错误。那如果我们定义一个特殊的成员变量呢?
如下示例,利用 mutable 来定义一个成员变量,并访问修改它的值会怎么样呢?基于以上代码,我们在上面两个成员变量前面加上 Mutable 来进行测试:
结果分析:
可以看到在定义变量时加上 Mutable 之后,当我们修改常函数中的变量的值时,就不会有问题出现了。
3.12.2 常对象
在常对象下,对于普通的成员变量是不可以修改它的值的,但是在普通成员变量前加上 mutable之后就可以被修改了。
再看下面的例子,证明常对象只能调用常函数。