本文写了作为一个习惯了Python的人员,学C++一些难受的地方和一些细节的不同。
当然,内部最多的还是C++基础的相关难点,同一个东西的各种不同写法,容易不小心犯的错误都罗列出来了,一些简单的东西如怎么写if什么是while都省略掉了。
如果你不清楚指针怎么用,或者函数传参配合上数组、指针、引用让你很苦恼,我有信心在这篇文章讲解的清清楚楚。此外后面的知识会穿插前面难点的复习。
一、基础部分
前面都是一些小菜,后面会省略掉python一些相关内容。
1.输出流
我们都知道C++中float类型7位有效数字,double类型15-16位有效数字。但对于这种小数输出时在不做任何设置的情况下,我们只会看到6位有效数字,即小数点前后有效数字加起来为6,小数点前是4位,那小数点后就是2位。
cout << 3.1415926535 << endl; // 3.14159
cout << 31.415926535 << endl; // 31.4159
cout << 314159.26535 << endl; // 3114159
若小数点前超过了6位,则会以科学计数法表示。
cout<< 3141592.6535 <<endl;// 3.14159e+06
对于这一点Python就好多了
print(3.1415926535897) # 3.1415926535897
那天一个学弟问我一道C语言的题,虽然这是道陈年老题,但还是要长个记性。
2.类型大小
我们都知道大部分的语言比如int类型都是占4个字节的,而python的int类型占24+4个字节,另外24个来自于其内部的封装,(列表数组我们后面再说)。当然,你像anaconda的一些科学计算库里面的数据类型,为了计算高效MinGW都是给你整的明明白白有大有小的。
cout << sizeof(100) << endl; //4
a = int()
print(a.__sizeof__()) # 24
b = int(100) # 即 b=100
print(b.__size0f__()) # 28
3.除法
对于计算,谁闲的会去把0当作除数或取模,两种语言都会报错。
对于C++,整数除以整数还是整数,整数/浮点数除以浮点数可能是整数可能是浮点数;
cout<< 3/2 << endl; // 1
cout<< 5/2.5 << endl; // 2
cout<< 5/2.2 << endl; // 2.27273
cout<< 0.5/0.25 <<endl; // 2
cout<< 0.5/0.22 <<endl; // 2.27273
cout<<0.5/5<<endl; // 0.1
哪怕你是用自己定义的变量来接收
float a1 = 5 / 2.5;
int a2 = 5 / 2.5;
float d1 = 2.5 / 5;
int d2 = 2.5 / 5;
cout << a1 << endl; // 2
cout << a2 << endl; // 2
cout << d1 << endl; // 0.5
cout << d2 << endl; // 0
float a = 0.5f //double float
对于python,注意是python3之后的。2还是3/2=1。对于整除Python用//表示。
print(3 / 2) # 1.5
print(5 / 2.5) # 2.0
print(0.5 / 0.25) # 2.0
print(0.5 / 0.22) # 2.272727272727273
3. 字符和字符串
对于c++,字符用单引号,字符串用双引号。python中单双三引号其实都可以。我本以为这就够了,有意思的是在学习JS的ES6时我还得知反引号可也用于包裹字符串。
char a = '哈';
char name1[] = "呃呃"; // 双引号
string name2 = "你好"; // #include <string> #include "string"
cout << name1<< "," <<name2 << endl;
4. 对于制表符
cout << "aaa\tbbbbb" << endl;
cout << "a\tbbbbb" << endl;
cout << "aaaaaa\tbbbbb" << endl;
/*
aaa bbbbb
a bbbbb
aaaaaa bbbbb
*/
print("aaa\tbbbbb")
print("a\tbbbbb")
print("aaaaaa\tbbbbb")
'''
aaa bbbbb
a bbbbb
aaaaaa bbbbb
'''
惊讶不,一个八位一个四位。
我更惊讶了,电脑端和手机端看到的不一样(来自iphone13用户)。
5. 对于输入
我们输入空格,意味该值我们已经输入完。
首先,如果你定义接收的类型和你输入的类型不符,那么它不接收。
int d;
float e;
cout<<d<<", "<<e<<endl;
cout<<"请输入一个整型,一个浮点型:"<<endl; //输入多了会影响到下一次
cin>>d>>e;
cout<<d<<", "<<e<<endl;
string f;
cout<<"输入一个字符串:"<<endl;
cin>>f;
cout<<"ok, "<<f<<endl; //有空格就不继续了
/*
0, 9.80909e-44
请输入一个整型,一个浮点型:
aa bb 123 33
0, 9.80909e-44
输入一个字符串:
ok,
*/
如果你输入的数量大于了你接收的数量,第一次还好,但多出来的部分会影响到第二次输出。
int d;
float e;
cout<<"请输入一个整型,一个浮点型:"<<endl; //输入多了会影响到下一次
cin>>d>>e;
cout<<d<<", "<<e<<endl;
string f;
cout<<"输入一个字符串:"<<endl;
cin>>f;
cout<<"ok, "<<f<<endl; //有空格就不继续了
/*
请输入一个整型,一个浮点型:
33 5.5 aaa
33, 5.5
输入一个字符串:
ok, aaa
*/
// 显然我第二次还没输入呢就打印出aaa了
所以没事别为难自己。
对于Python,你输入什么无所谓的,可以后续split处理也可以一开始map split接收多个值。
a = input('快点输入:')
print(a)
'''
快点输入: asd qwe2.1m 3 \n xxx
asd qwe2.1m 3 \n xxx
'''
6. 强制类型转换
前提是可以转换,“11”可以转换成数字11,“abc”就不能转换为数字。强制类型转换可以包括类型转换进制转换。
C++是在转换的前面加(类型)
python是直接如int(元素) 至于进制转换不知道的可以去查一下,不难。
7. 关于ASSIC码
char a='a';
cout << (int) a << endl; // 97
cout<< (char)65 <<endl; // A
print(ord('a'), chr(97)) # 97 a
8. 对于布尔类型的运算
在C++语言中,真返回1,假返回0。而python的真可能给你返回1、2、3、4等本身的值。
int g1 = 0 && 3; // 0
int g2 = 2 && 9; // 1
int h1 = 0||5; // 1
int h2 = 5||0; // 1
int h3 = 0||0; // 0
int t = 0||NULL; // 0
print(2 and 4) # 4
print(3 and 0) # 0
print(8 or 2) # 8
print(0 or None) # None
有意思的小障眼法:
print('False' and '0')
# 结果返回0,但这个0是字符串0,本式子为真,and两边字符串都不为空。
print(type('False' and '0')) # class str
9. 三目运算符
int a = 20, b = 30, c;
c = a > b ? a : b;
cout << c << endl; //30
a = 10 if 10>20 else 20
print(a) # 20
C++不能像python一样写推导式我好难受。
a = [i for i in range(5)]
print(a) # [0, 1, 2, 3, 4]
交换
a = 1
b = 2
a,b=b,a
print(a,b) # 2 1
10. 循环
C++不支持for-else,while-else。
python不支持switch,没有do-while。没有goto但是可以第三方库引入。
*11. C++数组、Py列表
C++:数组中的每个元素都是相同的数据类型,由连续的内存位置组成。
Py:元素可以是可以任意类型,连续的内存存贮每个元素的地址。
对于C++:
// 一维数组定义的三种方式
int arr1[5]; //赋值:arr1[0]=1 arr1[0]=2 arr1[0]=3 arr1[0]=4 arr1[0]=5
int arr2[5] = {1,2,3,4,5};
int arr3[] = {1,2,3,4,5};
// 类型 数组名[长度] = {值}
/*
对于第一种,不赋值直接输出结果比较随意,可能是0可能是其它数字。
对于第二种,假如定义长度大于所赋的值,范围内超出的为0.
第三种根据所赋的值自动定义长度。
此外,arr1[6],arr2[99],arr3[999]等这样超出范围的,结果随机。
*/
数组名:1、它可以统计元素在内存中的长度;2、它可以数组在内存中的首地址。
int arr1[5]; //赋值:arr1[0]=1 arr1[1]=2 arr1[2]=3 arr1[3]=4 arr1[4]=5
int arr2[8] = {1,2,3,4,5};
int arr3[] = {1,2,3,4,5};
cout<< sizeof(arr1)<< " "<< sizeof(arr2)<<' '<< sizeof(arr3)<<endl;
cout<<"arr2数组元素个数为: "<< sizeof(arr2)/ sizeof(int)<<endl;
cout<< arr1<< " "<< arr2<<' '<< arr3<<endl;
cout<< &arr1[0]<< " "<< &arr2[0]<<' '<< &arr3[0]<<endl;
/*
20 32 20
arr2数组元素个数为: 8
0x61fe00 0x61fde0 0x61fdc0
0x61fe00 0x61fde0 0x61fdc0
*/
对于Python:
lst = [1, 2, 3.5, 'nihao', ['呵呵'], {"xx": "oo"}]
print(lst) # [1, 2, 3.5, 'nihao', ['呵呵'], {'xx': 'oo'}]
# 比较自由,可以随便写,print(列表名)为该列表内所有内容。
# 顺便提一下py超过了范围与C++不同会报错
我们知道C++中输出地址时是16进制的数字,而Python中则是10进制进的。
a = [1, 2, 3.5, 'nihao', ['呵呵'], {"xx": "oo"}]
print(id(1)) # 140724378797744
print(id(a), id(a[0])) # 1590331192512 140724378797744
C++数组要求元素类型统一,比如统一为int类型,占4个字节,那么它的下一个元素比如a[0]和a[1]就差4字节,又规定过长度,还是连续的空间,所以比较基础。
而python中各个类型以对象的形式封装,且列表可以存储多个元素,这导致长度无法确定。因此Python中选用一块连续的内存,内存中存贮着不同地区元素的地址。且其实python的列表也是需要规定初始长度的,这些内部C语言以及帮我们完成了。
Python垃圾回收机制_suic009的博客-CSDN博客
C++不能直接切片。
C++无法直接通过等于号像Python的列表一样进行数组的拷贝(同一块内存地址),不同需要lst.copy()、tup.deepcopy()。
*12. 二维数组
python我们就不说了没什么意思,往深了研究还涉及数据池,深浅拷贝等。主要看下C++:
定义方式:
//喜欢哪种用哪种
int arr1[2][3];
int arr2[2][3] = {{1, 2, 3},{4, 5, 6}};
int arr3[2][3] = {1, 2, 3, 4, 5, 6};
int arr4[][3] = {1, 2, 3, 4, 5, 6};
double arr2[2][3] = {{1, 2, 3},{4, 5, 6}};
cout<<arr2[0][0]<<' '<<arr2[0][1]<<endl; // 结果依然为 1 2
数组名:
arr arr[0] arr[0][0] 地址都是一样的,arr[0]其实就是个一维数组。
cout<<arr3<<' '<< arr3[0] << ' '<<&arr3[0][0] <<endl; // 0x61fdc0 0x61fdc0 0x61fdc0
cout<<"元素个数为:"<< sizeof(arr3)/ sizeof(int)<<endl; // 6
cout<<"数组行数为:"<< sizeof(arr3)/ sizeof(arr3[0])<<endl; // 2
cout<<"数组列数为:"<< sizeof(arr3[0])/ sizeof(arr3[0][0])<<endl; // 3
*13. 函数
在C++中,你可以这样理解:
对于return 13:我们平时定义变量赋值:int a = 13; 这次return显然没有用int限制,所以在函数声明时要告诉它返回值类型。不写return 就写void记得别接收就行,写就记得带上int、float等。
注意:不只有int float void等,这个要看你的返回值是什么。如果是个对象,那就要用类去声明。
float foo(){
float a = 1.12;
return a;
}
int main() {
cout<<foo()<<endl;
return 0;
}
// 结果输出 1.12
// 如果把float foo 改为int foo 则输出1
注意:
- 用什么声明函数就用什么接收函数的返回值
- 函数声明和函数的返回值return后写的东西要遵循声明变量的方法。
如 我声明一个想用一个对象去接收的函数的返回值,Person p = foo(); 那这个foo应该这样声明: Person foo(){ return p}。想声明指针 int *p = foo();那就 int * foo(){return &a} ,想引用 int&a=foo(); 那就 int& foo(){return a}
值传递:
可以看到下面当形参发生改变不会影响到实参,上面的a,b是开辟了新的内存存储1和2.
(数组指针等的传递后面会将,这里先看值传递。)
void foo(int a,int b){
cout<<&a<<' '<<&b<<endl;
a = 100;
}
int main() {
int a=1,b=2;
cout<<&a<<' '<<&b<<endl;
foo(a,b);
cout<<a<<endl;
return 0;
}
/*
0x61fe1c 0x61fe18
0x61fdf0 0x61fdf8
1
*/
在python中虽然这里看到地址一样当值依然不会变,涉及的优化知识我上面放的链接有,感兴趣的可以看一下。
def foo1(a,b):
print(id(a),id(b))
a = 100
def foo2():
a = 3
b = 4
print(id(a),id(b))
foo1(a,b)
print(a,b)
foo2()
'''
140724378797808 140724378797840
140724378797808 140724378797840
3 4
'''
函数声明:
int foo(int a,int b);
Python不需要函数声明也可以。
def foo1():
foo2()
pass
# foo1() 从上到下执行,此时还没见过foo2(),报错。
def foo2():
print('ok')
pass
foo1() # ok
C++的分文件编写:
C++的那些库就像Python的库/模块/包一样。
所以分文件就像Python导入自己写的模块一样,里面可以写类、函数、全局变量等。
1. 创建后缀名为.h的头文件 2. 创建后缀名为.cpp的源文件 3. 在头文件中写函数声明 4. 在源文件中写函数定义
这里我用的是Cloin编写的,用VS的其实是一样的,该加的我都标出来了。
# foo.h
# foo.cpp
在main.cpp里导入刚刚写的
运行结果如下:
ps: Cloin新手用户小指南,读一读英文就懂了。
*14. 指针
指针的说明定义和使用
前面说了对于一个变量比如int a =10;那么内存空间0x0000到0x0004用来存储它。实际上我们在定义它的时候并不知道它的地址是多少,所以我们给它起了个名字a。a的地址&a即0x0000。
但当我们知道它的地址或者要操作它的地址时,就可以用指针来做。
可以看到,就像变量a保存了10一样,我们的指针变量p保存了a数据的地址。
我们定义一个int * 类型的变量p用来存储&a。
要分清 p,*p,int *
注意:
int *p; 这样看:int *是个数据类型 (int *)p 定义一个指针变量p,64位操作系统p占8字节。
int *p=&a; 这样看:(int *)(p=&a); 即把&a赋值给变量p,用int *声明这是个指针变量。
cout<<sizeof(int *)<<endl; // 8
cout<<sizeof(float *)<<endl; // 8
cout<<sizeof(double *)<<endl; // 8
对于*p,把*和p分开看。p变量保存地址,*指向这个地址(即取到这个地址的值)。
*p是这个地址指向的结果,所保存的数据。
int main() {
int t[2][3] = { 1,2,3,4,5,6 };
cout << t << " " << t[0] << " " << &t[0][0] << " " << &t<<endl;
int* p1 = t; // 错 如果t是一维数组可以这样写
int* p2 = t[0]; // 可以 *p为1 p为它的地址 其实t[0]是一个一维数组
int* p3 = t[0][0]; // 错
int* p3 = &t[0][0]; // 可以;
return 0;
}
如果是数组arr,应写int *p=arr;而不是 int *p=&arr;虽然cout出来的arr和&arr是相等的,但是地址的地址显然是不对的,但可以写int *p=&arr[0]。
int main() {
int a = 10;
int *p;
p = &a;
cout<<"a的地址为:"<<&a<<endl;
cout<<"p的值为:"<<p<<endl;
cout<<"*p为:"<<*p<<endl;
}
/*
a的地址为:0x61fe14
p的值为:0x61fe14
*p为:10
*/
为了加深理解,我们还可以把*p看成一个整体,其实此时*p就是10,我们可以:
int c = *p;
cout<<c<<endl; // 10
这一步是先根据p中的地址*去取到,此时*p为10,10作为变量赋值给了c,感觉不难吧。
知道了上面的这些,指针这样直接定义,可就能看懂了:
int *p = &a; // (int *)(p=&a);
但要注意,只有在声明时可以这样写,因为int * 与p=&a是分开的,千万别后面再次用到或者改变值的时候写 *p=&a。这是要让*p这个值改为&a即a的地址吗?
回过头来看一下,我们p与&a相等,*p与a相等,我们用a对a内存的数组进行修改是肯定能改的。我们*p和a指向的是同一块内存,因此*p的赋值修改也会影响a。此外我们最初学的a其实和*p的*那步是一样的,都是拿着地址去取值。
int a = 10;
int *p = &a;
*p = 99;
cout<<"a为:"<<a<<" *p为:"<<*p<<endl; // a为:99 *p为:99
int a 告诉系统要4个字节的空间存a,int * 则在32位(x86)操作系统使p占4个字节,64位(x64)操作系统使p占8个字节。
int a = 10;
int *p = &a;
cout<<sizeof(p)<<endl; // 64位操作系统,p占8字节
cout<<sizeof(*p)<<endl; // 10是int类型占4字节
空指针
指针变量指向内存空间编号为0的指针叫空指针。0x0000。
一般用于初始化指针变量,记得空指针的内存不可访问。
int *p = NULL;
cout<<"p为:"<<p<<" *p为:"<<*p<<endl; // p为:0 *p为:
// 上面输出就是那样 没少写
// Process finished with exit code -1073741819 (0xC0000005)
// 当然了用VS就崩了
编号0-255是系统占用的,访问会出错。
野指针
我们之前给p赋的是地址 int *p=&a。 如果我们这样写肯定会报错(int *p=0x0000除外):int *p=0x1010程序无法执行,这是在让p指向这个十六进制数字,假如我们通过强制类型转换:int *p=(int *)0x1010
int *p = (int *)0x1100;
cout<<"p为:"<<p<<" *p为:"<<*p<<"输出结束"<<endl; // p为:0 *p为:
//p为:0x1100 *p为:
//Process finished with exit code -1073741819 (0xC0000005)
这块乱写的地址没有去申请还去访问它肯定是不行的。
const修饰指针
- const修饰指针
- const修饰常量
- const即修饰指针又修饰常量
其实看const修饰谁谁就不变就行了,理解上面p,*p,int *很好理解什么是什么。
const修饰指针 指针指向的值不可以改但指针的指向可以改。
int a=10;
int b = 20;
const int *p = &a;
// 此时*p=99不行 但p=&b可以 再强调一下别写成*p=&b
const修饰常量 指针指向的值可以改但指针的指向不可以改。
int a=10;
int b = 20;
int * const p = &a; // int const *p = &a; 也不报错 但最好还是int *一起。。
// 此时p不能变,即指针的指向不能变,p=&b错,*p=99可以。
const即修饰指针又修饰常量
那就都不能变呗。
指针和数组
如果把上面的知识看懂的话这里就很好理解了
int arr[5]={1,2,3,4,5};
int *p=arr; // int *p=&arr[0]一样的 但 int *p=&arr是错的
cout<<"第一个数据为:"<<*p<<endl; // 第一个数据为:1
我们遍历一下:
for(int i=0;i<5;i++){
cout<<"第"<<i+1<<"个数据为:"<<*p<<endl;
p+=1; // 往后移四个字节
}
/*
第1个数据为:1
第2个数据为:2
第3个数据为:3
第4个数据为:4
第5个数据为:5
*/
指针与函数
我们上面知道通过值传递,形参的改变无法影响到实参,因为其开辟了属于自己的空间。
void swap(int a,int b ){
int tem = a;
a = b;
b = tem;
cout<<a<<' '<<b<<endl; // 2 1
}
int main() {
int a = 1;
int b =2;
swap(a,b);
cout<<a<<' '<<b<<endl; // 1 2
}
但若形参实参共用同一块地址,因为变量的指向相同,所以形参的改变必会引起实参的变化:
void swap(int *a,int *b ){
int tem = *a;
*a = *b;
*b = tem;
cout<<*a<<' '<<*b<<endl; // 2 1
}
int main() {
int a = 1;
int b =2;
swap(&a,&b);
cout<<a<<' '<<b<<endl; // 2 1
}
注:传递地址的时候这一步相当于:int *a=&a; 注 前后两个a不是一个东西,前形参后实参,两块空间。形参a里存着实参a的地址。*a指向这个值。
int *foo(){
int a = 1;
// int arr[5];
return &a;
// return arr;
}
这样是正确的但没必要,栈区被清理,main中只能调用一次,后面学的引用也是,不建议:
(栈区、引用 在后面有写,不急)
指针函数与数组
void details(int arr[]) { // *arr
cout<<arr<<" "<<arr[0]<<endl; // 0x61fe00 1
}
int main() {
int arr[5]={1,2,3,4,5};
cout<<arr<<" "<<arr[0]<<endl; // 0x61fe00 1
details(arr); // 记得这里不要写&arr 以后数组就写名,值就写&变量。
}
形参接收函数时,要么写arr[] 要么写*arr,这俩一样的。写arr会报错,普通变量怎么能接收地址呢。
因此在C++中,int、float等类型默认值传递,想要改变就把地址传进去用指针接收。数组类型你传递的时候不得不用指针接收,就是一定会改变原内容。
在python中
1.函数传参过程中,对于一些基本数据类型,如int(整型),float(浮点型),str(字符串)等,是值传递,函数内部对以上数据类型的数据进行修改时并不会改变原值。
2.对于list(列表)、dict(字典)、tuple(元组)则是地址传递,函数内部对以上数据类型操作时会改变原数据值。
小案例
用C++写一个功能函数,实现冒泡排序,要求数组在原地址上发生改变,而不是通过接收返回值来改变。
#include <iostream>
using namespace std;
void swap(int *a,int *b ){
int tem = *a;
*a = *b;
*b = tem;
}
void bulleSort(int *arr,int length ){ // arr[]
int flage;
for(int i=0;i<length-1;i++){
flage = true;
for(int j=0;j<length-i-1;j++){
if(arr[j]>arr[j+1]){
swap(&arr[j],&arr[j+1]);
flage = false;
}
}
if(flage){
return; // 冒泡优化,没发生换位那就终止。
}
}
}
int main() {
int arr[10] = {1,5,3,6,6,8,3,2,9,2};
int length = sizeof(arr)/sizeof(int);
bulleSort(arr,length); // &arr[0]
for(int i=0;i<length;i++){
cout<<arr[i]<<endl;
}
}
15. 结构体
结构体是属于用户自定义的数据类型,允许存储不同数据。
语法:struct 结构体名 {结构体成员};
通过结构体创建变量的三种方式:
- struct 结构体名 变量名
- struct 结构体名 变量名={成员1值,成员2值...}
- 定义结构体时顺便创建变量。
struct Student{
string name;
int age;
int score;
}s3;
int main() {
// 法1
struct Student s1;
s1.name = "李四";
s1.age = 50;
s1.score = 100;
cout<<s1.name<<" "<<s1.age<<" "<<s1.score<<endl;
// 法2
struct Student s2={"张三",18,100};
cout<<s2.name<<" "<<s2.age<<" "<<s2.score<<endl;
// 法3 在上面末尾定义
s3.name = "王五";
s3.age = 10;
s3.score = 20;
cout<<s3.name<<" "<<s3.age<<" "<<s3.score<<endl;
}
结构体数组
上面我们只是定义了一个一个的学生,实际上多个学生的信息我们是要放到一起的,
语法:struct 结构体名 数组名[个数] = {{}, {}, {}...}
struct Student{
string name;
int age;
int score;
};
int main() {
struct Student arr[5]={
{"张三",18,100},
{"李四",50,50},
{"王五",12,30}
}; //范围内访问超出元素个数的如arr[3] 字符串不显示 数字为0
arr[1].name = "李十四";
cout<<arr[1].name<<"的分数为:"<<arr[1].score<<endl;
}
结构体指针
通过操作符 -> 可以通过结构体指针访问结构体属性,后面学的类对象也是。
记得声明结构体指针时是 (struct) 结构体名 *p=&元素/数组名 即声明xx结构体类型的指针变量。
int main() {
struct Student s={"张三",18,100}; // Student s={"张三",18,100};
struct Student *p = &s; // Student *p = &s;
cout<<p->name<<"的分数为:"<<p->score<<endl; // 张三的分数为:100
}
如果你写的是p.name 它会自动帮你转换成p->name,明白指针的话(*p).score也行。
结构体嵌套
struct Student{
string name;
int age;
int score;
};
struct Teacher{
int id;
string name;
int age;
struct Student stu;
};
int main() {
struct Student s1={"张三",18,95};
struct Teacher t = {001,"李老师",80,s1};
t.stu.name = "张三万";
cout<<t.name<<"的学生"<<t.stu.name<<"的分数为:"<<t.stu.score<<endl;
// 李老师的学生张三万的分数为:95
}
struct Student{
string name;
int age;
int score;
};
struct Teacher{
int id;
string name;
int age;
struct Student arr[5];
};
int main() {
struct Student s1={"张三",18,95};
struct Student s2={"张四",19,97};
struct Student s3={"张五",20,100};
struct Teacher t = {001,"李老师",80,s1,s2,s3};
for(int i=0;i<3;i++){
cout<<t.name<<"的学生"<<t.arr[i].name<<"的分数为:"<<t.arr[i].score<<endl;
}
/*
李老师的学生张三的分数为:95
李老师的学生张四的分数为:97
李老师的学生张五的分数为:100
*/
}
当然也可以通过for循环给t.arr[i]赋值或者 t.arr[i].name等的赋值。
struct Student{
string name;
int age;
int score;
};
struct Class{
int id;
struct Student arr[5];
};
struct Teacher{
int id;
string name;
int age;
struct Class c;
};
int main() {
struct Student s1={"张三",18,95};
struct Student s2={"张四",19,97};
struct Student s3={"张五",20,100};
struct Class c1={001,s1,s2,s3};
struct Teacher t = {001,"李老师",80,c1};
for(int i=0;i<3;i++){
cout<<t.name<<"的"<<t.c.id<<"班"<<"学生"<<t.c.arr[i].name<<"的分数为:"<<t.c.arr[i].score<<endl;
}
/*
李老师的1班学生张三的分数为:95
李老师的1班学生张四的分数为:97
李老师的1班学生张五的分数为:100
*/
}
一般用结构体指针接收结构体数组:
struct Student{
string name;
int age;
int score;
};
struct Class{
int id;
struct Student *stu;
};
int main() {
struct Student s[3]={
{"张五",20,100},
{"张四",19,97},
{"张五",20,100}
};
struct Class c1={001,s};
for(int i=0;i<3;i++){
cout<<c1.id<<"班的"<<"学生"<<c1.stu[i].name<<"的分数为:"<<c1.stu[i].score<<endl;
}
/*
1班的学生张五的分数为:100
1班的学生张四的分数为:97
1班的学生张五的分数为:100
*/
}
这样只开辟一个指针变量的大小,用别人家地址存放的东西,节省空间不是吗。
结构体做函数参数
值传递
struct Student{
string name;
int age;
int score;
};
void details(Student stu){ // struct Student stu
cout<< stu.name<<","<<stu.age<<","<<stu.score<<endl;
stu.name="xxxxxx";
}
// 值传递
int main() {
struct Student s1={"张五",20,100};
details(s1); // 张五,20,100
}
地址传递 节省空间
void details(Student *stu){
// 这个时候只能用箭头
cout<< stu->name<<","<<stu->age<<","<<stu->score<<endl;
// cout << (*stu).name << "," << (*stu).age << "," << (*stu).score << endl; 这样也行
stu->name="xxxxxx";
}
// 地址传递
int main() {
struct Student s1={"张五",20,100};
details(&s1);
cout<< s1.name<<","<<s1.age<<","<<s1.score<<endl; // xxxxxx,20,100
}
地址传递数组(结构体数组也是数组,只是内部元素不同)
void details(Student *stu) { // 指针指向这个数组第一个
for (int i = 0; i < 3; i++) {
// 这个时候不能用箭头
cout << stu[i].name << "," << stu[i].age << "," << stu[i].score << endl;
stu[i].name = "xxxxxx";
}
}
int main() {
struct Student s[3] = {
{"张五", 20, 100},
{"张四", 19, 97},
{"张五", 20, 100}
};
details(s); // 张五,20,100 再强调一下虽然s和&s cout出来的值一样,这里也不能写&s
for(int i = 0; i < 3; i++) {
cout << s[i].name << "," << s[i].age << "," << s[i].score << endl;
}
}
/*
张五,20,100
张四,19,97
张五,20,100
xxxxxx,20,100
xxxxxx,19,97
xxxxxx,20,100
*/
结构体作为返回值
struct Student {
int id;
string name;
};
Student foo() {
Student s = { 99,"xx" };
return s;
}
int main() {
Student p = foo();
cout << p.name << endl;
}
愿意指针引用也行,但没必要,不做其他声明也只能用一次显然不好。后面讲类对象的时候会练,这里先略。
结构体中const应用场景
将函数中的形参改为指针,可以减少内存空间,而且不会复制新的副本出来。(指针只占8个字节,而如果是重新形参Student stu接收,要开辟该结构体一样大小的空间。)此时也就是所谓的地址传递,形参改变会改变实参。
为了防止误操作,可以加个const。即我们前面讲的const修饰指针(指向的值不可以变)。
这一看到这里飘红了。
当然这样也不对
当然这样就行了
要明白在数组那讲的const修饰指针修饰的是什么地方。前两个是地址对应的值,后面那个是地址。
二、C++进阶
1. 内存分区模型
C++程序在执行时,将内存大方向划分为4个区域:
- 代码区:存放函数体的二进制代码,又操作系统进行管理的(程序运行前)
- 全局区:存放全局变量和静态变量以及常量(程序运行前)
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等(程序运行后)
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。(程序运行后)
内存四区的意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域:
代码区:
存放cpu执行的机器指令
代码区是 共享 的,目的是对于频繁被执行的程序,只需在内存中有一份代码即可。
代码区是 只读 的,使其只读的原因是防止程序意外地修改了它的指令。
全局区:
全局变量和静态变量存放在此。
全局区还包含了常量区,字符串常量和其他常量也存放在此。
该区域地数据在程序结束后由操作系统释放。
通过打印地址可以发现,局部变量和局部常量地址相近;
全局变量、全局常量静态变量字符串常量地地址相近。
程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值,局部变量等。
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
注意返回地址声明时要声明为指针类型。
int* func() {
int a = 10;
return &a;
}
int main() {
int* p = func();
cout << *p << endl; //10
cout << *p << endl; //2025425288
}
我们上面知道,编译器自动分配释放栈区数据,我们最后返回a的地址,此时func函数结束,里面的数据被释放。我们下面再通过*p去取的时候,该块内存以及没有访问权限或者访问出乱码了。
之所以第一次嫩更成功,是因为此时编译器给我们做了一次保留,担心是我们误操作。但第二次就不保留了。(可以static int a = 10; )
堆区:
- 有程序员分配释放,若程序员不释放,程序结束之后有操作系统回收
- 在C++中主要利用new在堆区中开辟内存
注意返回地址声明时要声明为指针类型。
int* func() {
//new返回该数据类型的指针,所以用指针接收 new的是float就用 float *接收
int *a = new int(10);
return a; // a存的是堆区数据的地址,他返回给主函数的*p去接收。
}
int main() {
int* p = func(); // 这里的int *p=a 相当于以前的 int a=10; int *p=&a;
cout << *p << endl; // 10
cout << *p << endl; // 10
delete p;
cout << *p << endl; // 引发异常
}
int* func() {
int *arr = new int[10]; //数组中括号
arr[0] = 99;
return arr; // arr是数组首地址,他返回给主函数的*p去接收。
}
int main() {
int* p = func();
cout << p[0] << endl; // 99
cout << p[0] << endl; // 99
delete[] p; // 释放数组全部空间
}
这样又多了种定义变量和数组的方式 int *arr = new int[10];其中arr未数组名,也是它的地址。
这里new和delete的知识后面讲析构函数还会用到。
2. 引用
int main() {
int a = 10;
int& b = a; // int * const b = &a;
cout << b << endl; // 10
b = 99; // *b = 99;
cout <<a<<" "<< b << endl; // 99 99
}
这个怎么理解呢,以前存放10的这块地址叫a,以后也叫b了。即此时a,b都是变量,10的名字。
注意,如果写成了int b = &a;会报错(你怎么能让一个16进制的数字去赋值给一个int类型的变量呢?),别和前面学的指针 int * b = &a;搞混。
int main() {
int a = 10;
int *b = &a;
cout << b << endl; // 00AFF804
*b = 99;
cout <<a<<" "<< *b << endl; // 99 99
}
- (开始定义时) 引用必须要初始化。(define、const也是 但const int &a去做函数参数可以不)
- 引用之后就不可以改变。
int main() {
int a = 10,c=100;
int &b = a;
b = c; // 相当于执行 a=100或者说是b=100
cout << b << endl; // 100 这是进行赋值,而不是更改引用。
b = 99;
cout <<a<<" "<< b << endl; // 99 99
}
可以用引用代替指针接收
void swap1(int& a, int& b) {
int tem = a;
a = b;
b = tem;
}
void swap2(int* a, int* b) {
int tem = *a;
*a = *b;
*b = tem;
}
int main() {
int a = 3;
int b = 5;
swap1(a, b);
cout <<a<<" "<< b << endl; // 5 3
swap2(&a, &b);
cout << a << " " << b << endl; // 3 5
}
引用做函数返回值
- 不要返回局部变量的引用
- 函数可以是左值
int& foo1() {
int a = 10; // 局部变量 栈区 这个函数结束后释放
return a;
}
int& foo2() {
static int a = 99; // 静态变量 全局区 程序结束后释放
return a;
}
int main() {
int& a = foo1();
cout << a << endl; // 10 编译器做了保留
cout << a << endl; // 2038794632 内存已经被释放
int& b = foo2();
cout << b << endl; // 99
cout << b << endl; // 99
foo2() = 10000; // 其实就是赋值 a=10000;
cout << b << endl; // 10000
}
注意
int& foo2() {
static int a = 99; // 静态变量 全局区 程序结束后释放
return a;
}
像这样用&声明名的函数,返回的内容还是a,foo2()结果依然是99,但此时你不能用int &类型的变量去接收,即int &b = foo2()是错的,即int &b = 99是错的,但const int &b = foo2()可以。 所以如果我像直接int &b = foo2()那就在函数声明时加上&,要么就 int b=foo2()一个最简单的接收函数返回值。
const修饰防止误操作
// int a = 99;
// int &b = a; //可以
// int &b = 5; 不能这样写
const int& b = 10; // 可以 不可修改
// const int *p = 5; int* const p = 10; 不可以
void foo1(const int &a) { // 加个const防止以后操作的时候不小心改变
//用a接收而不是&a相当于 a = 1000; 形参 值传递了,不是引用了.
cout << a << endl;
}
int main() {
int a = 10;
foo1(a);
}
3. 函数高级
默认参数
如果函数声明时有默认参数,函数实现就不能有默认参数。二者只能有一个有。
void foo1(int a = 1, int b = 2);
void foo1(int a=1,int b =2) {
cout << a <<" "<<b << endl;
}
int main() {
int a = 10,b=20;
foo1();
}
则
函数占位参数
// 目前阶段的展位参数我们还用不到取不到,以后会将。
// 占位参数也有默认参数 void foo1(int a,int=10) {}
void foo1(int a,int) {
cout << a << endl;
}
int main() {
int a = 10,b=20;
foo1(a,b); // 此时必须传两个
}
函数重载
函数名可以相同,提高复用性
void foo(int a) {
cout <<"重载函数1 "<< a << endl;
}
void foo(int a,int b) {
cout << "重载函数2 " << a <<" "<< b << endl;
}
void foo(float a, float b) {
cout << "重载函数3 " << a << " " << b << endl;
}
void foo(int a, float b) {
cout << "重载函数4 " << a << " " << b << endl;
}
int main() {
int a = 10,b=20;
float c = 1.2, d = 3.14;
foo(a);
foo(a,b);
foo(c,d);
foo(a,d);
}
/*
重载函数1 10
重载函数2 10 20
重载函数3 1.2 3.14
重载函数4 10 3.14
*/
注:函数的返回类型返回值不能作为重载条件,看参数就行了。
函数重载注意事项
- 引用作为注意事项
- 有默认值的函数重载
引用作为注意事项
void foo(int &a) {
cout <<"重载函数1 "<< a << endl;
}
void foo(const int &a) {
cout << "重载函数2 " << a << endl;
}
int main() {
int a = 10;
foo(a); // 重载函数1 10
}
这种情况会走函数1,无论1 2 谁在前。
void foo(int &a) {
cout <<"重载函数1 "<< a << endl;
}
void foo(const int &a) {
cout << "重载函数2 " << a << endl;
}
int main() {
int a = 10;
foo(10); // 重载函数2 10
}
这种情况直接传10,走函数2。 因为走函数1不合法呗。
有默认值的函数重载
void foo(int a) {
cout <<"重载函数1 "<< a << endl;
}
void foo(int a=99) {
cout << "重载函数2 " << a << endl;
}
int main() {
int a = 10;
foo(a);
}
这样不可以 报错
void foo(int a, int b = 20) {
cout <<"重载函数1 "<< a << endl;
}
void foo(int a=99) {
cout << "重载函数2 " << a << endl;
}
int main() {
int a = 10;
foo(a);
// foo(30,40) 可以
}
也不行 还报错,因为都能走。尽量避免把。
4. 类和对象
C++面向对象三大特征:封装继承多态。
语法 class 类名 {访问权限:属性/行为};
封装
设计一个圆类,显示周长。
#include<iostream>
#include<string.h>
using namespace std;
#define PI 3.1415926
class Circle {
public:
int r;
float calculate() {
return 2 * PI * r;
}
};
int main() {
Circle c;
c.r = 1;
cout << c.calculate() << endl;// 6.28319
}
内部赋值/修改
设计一个学生类(小坑)
#include<iostream>
#include<string.h>
using namespace std;
#define PI 3.1415926
class Student {
public:
int id;
string name;
void setS(int id1,string name1) { // 注意 这里不可以写一样的名字C++不认识,Py认识。
id = id1;
name = name1;
}
void show() {
cout << id << " " << name << endl;
}
};
int main() {
Student s1;
s1.id = 1;
s1.name = "张三";
s1.show(); // 1 张三
Student s2;
s2.setS(2, "李四"); // 如果写了 id=id name = name 下面就会输出乱码
s2.show(); // 2 李四
}
公有、私有、保护权限
public 类内可以访问 类外可以访问 子类可以访问
private 类内可以访问 类外不可以访问 子类不可以访问
protected 类内可以访问 类外不可以访问 子类可以访问
class与struct
- class默认权限是私有权限。
- struct默认权限是公有权限。
成员属性私有化
这个还有有必要的,我们可以自己控制哪些可读哪些可写。
对于一些写的权限,我们可以进行校验,防止超出有效范围。
include<iostream>
#include<string.h>
using namespace std;
class People {
int id; // 只读
string name; // 可读可写
int age; // 可读可写
public:
People(int i=000, string n="未实名", int a=18) { // 这里以后讲 先写着玩玩。
id = i;
name = n;
age = a;
}
void show() {
cout << id <<" "<< name <<" "<< age << endl;
}
int show_id() {
return id;
}
string show_name() {
return name;
}
int show_age() {
return age;
}
void set_name(string n) {
name = n;
}
void set_age(int a) {
if(a<0 || a>150){
cout << "- - gun - -" << endl;
return;
}
age = a;
}
};
int main() {
People p1(001,"张三",20);
People p2(002,"李四");
p1.show();
p2.set_age(200);
p2.show();
}
/*
1 张三 20
- - gun - -
2 李四 18
*/
C++不能直接打印出 cout << p << endl;
打印地址 cout << (int * )&p << endl;
对象的初始化和清理
C++利用构造函数和析构函数解决对象的初始化和清理问题。
注:这两个就算你不写,编译器自己调用自己空的。还有个拷贝构造函数也是默认的。也就是说有3个,默认构造函数、析构函数、拷贝构造函数是C++编译器自己添加的。
构造函数
主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
- 构造函数没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在创建对象的时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数
主要作用在于对象销毁前系统的自动调用,执行一些清理工作。
- 析构函数没有返回值也不写void
- 函数名称与类名相同,在名称前加上~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include<iostream>
#include<string.h>
using namespace std;
class People {
public:
People() {
cout << "你好" << endl;
}
~People()
{
cout << "拜拜" << endl;
}
};
// 构造和析构都是必须实现的 如果我们不写 编译器会提供一个空实现的构造和析构
int main() {
People p1; // 在栈上的数据 main函数执行完就会释放
}
/*
你好
拜拜
*/
构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
People(const People &p) { // 引用接收 不能把它本身改了呀所以加个const
// 这个p是另一个人对象
num = p.num;
cout << "拷贝构造函数" << endl;
}
注意:构造函数(拷贝)用People &p接收参数可以,People p错(难道你自己写的构造函数构造你自己吗?)。
在其他函数中People p可以是值传递。
调用构造函数
法1:括号法
#include<iostream>
#include<string.h>
using namespace std;
class People {
public:
int num;
People() {
cout << "普通:无参构造/默认构造" << endl;
}
People(int a) {
cout << "普通:有参构造" << endl;
}
People(const People &p) { // 不能把它本身改了呀
// 这个p是另一个人对象
num = p.num;
cout << "拷贝构造函数" << endl;
}
~People()
{
cout << "拜拜" << endl;
}
};
int main() {
People p1; // 默认构造函数
People p2(99); // 有参构造函数
People p3(p2); // 拷贝构造函数 此时p3的姓名年龄等与p1一样
}
/*
普通:无参构造/默认构造
普通:有参构造
拷贝构造函数
拜拜
拜拜
拜拜
*/
注意:调用默认构造函数时,不要加()。 python写多了真是不好改。
因为下面这行代码,编译器会认为是一个函数声明。
People p(); // 并不会有什么报错或者输出的对象或者初始化了声明对象
法2:显示法:
int main() {
People p1; // 默认构造函数
People p2 = People(99); // 有参构造函数
People p3 = People(p2); // 拷贝构造函数
}
注意:
- People(99); 匿名对象 当前程序结束后,系统会立即回收掉匿名对象。
- 不要用拷贝构造函数初始化匿名对象 People(99) === People p3; 对象重定义。
法3:隐式转换法:
int main() {
People p1; // 默认构造函数
People p2 = 99; // 有参构造函数
People p3 = p2 ; // 拷贝构造函数
}
拷贝构造函数调用的三种情况
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值 (People p接收参数值传递相当于给形参赋值;ps任何形式拷贝构造函数中只能以People &p接收)
- 以值方式返回局部对象 (含有和接收return p返回值时赋值);
情况1:
int main() {
People p1; // 默认构造函数
People p2(p1) ; // 拷贝构造函数
}
情况2: 函数用 People p接收
#include<iostream>
#include<string.h>
using namespace std;
class People {
public:
int num;
People() {
cout << "普通:无参构造/默认构造" << endl;
}
People(int a) {
num = a;
cout << "普通:有参构造" << endl;
}
People(const People &p) {
num = p.num;
cout << "拷贝构造函数" << endl;
}
~People()
{
cout << "拜拜" << endl;
}
};
void foo(People p) { // 值传递
cout << p.num << endl;
p.num = 0;
cout << p.num << endl;
}
int main() {
People p1(99);
cout << p1.num << endl;
foo(p1);
cout << p1.num << endl;
}
/*
普通:有参构造
99
拷贝构造函数
99
0
拜拜
99
拜拜
*/
情况3: 含有和接收return p返回值时赋值;
#include<iostream>
#include<string.h>
using namespace std;
class People {
public:
int num;
People() {
cout << "普通:无参构造/默认构造" << endl;
}
People(int a) {
num = a;
cout << "普通:有参构造" << endl;
}
People(const People& p) {
num = p.num;
cout << "拷贝构造函数" << endl;
}
~People()
{
cout << "拜拜" << endl;
}
};
People foo() { // 注意是People 声明函数不止int float等
People p(99);
return p;
}
int main() {
foo(); // 匿名对象 直接释放(直接析构)
cout << "ok" << endl;
//cout << p.num << endl;
}
/*
普通:有参构造
拷贝构造函数
拜拜
拜拜
ok
*/
在释放前执行的拷贝构造函数
#include<iostream>
#include<string.h>
using namespace std;
class People {
public:
int num;
People() {
cout << "普通:无参构造/默认构造" << endl;
}
People(int a) {
num = a;
cout << "普通:有参构造" << endl;
}
People(const People &p) {
num = p.num;
cout << "拷贝构造函数" << endl;
}
~People()
{
cout << "拜拜" << endl;
}
};
People foo() {
People p(99);
return p;
}
int main() {
People p = foo(); // foo()先拷贝构造函数生成匿名对象
cout << p.num << endl;
}
/*
普通:有参构造
拷贝构造函数
拜拜
99
拜拜
*/
People foo() {
People p(99);
return p;
}
int main() {
People p = foo(); // 匿名对象 直接释放
cout << "ok" << endl;
//cout << p.num << endl;
}
/*
普通:有参构造
拷贝构造函数
拜拜
ok
拜拜
*/
别急 再玩一下:
(上面是值传递 )
引用返回
People &foo() {
People p(99);
return p;
}
int main() {
People &p = foo();
// 当然了 要是People p = foo(); 就调用拷贝构造函数了,与声明函数时People foo() 无异
cout << p.num << endl;
}
/*
普通:有参构造
拜拜
99
*/
指针
People *foo() {
People p(99);
return &p; // 这里只能&p 说明对象不像数组一样名字就是地址
}
int main() {
People *p = foo();
cout << (*p).num << endl; // 这里要加括号 不然报错执行顺序不一样
// cout << p->num << endl; 正常应该这样写
}
/*
普通:有参构造
拜拜
99
*/
我们可以看到,对于情况3的三种情况:
- 第一种:People p = foo(); 用p去接收一个对象p`,相当于 People p = p`; 执行拷贝构造函数。
- 第二种:People &p = foo(); 这个是引用,相当于People &p = p`;给这个起了个别名,不执行拷贝构造函数。
- 第三种:People *p = foo(); 涉及知识点毕竟较多,上面注释都写了,不执行拷贝构造函数。
2、3种不进行对象与对象之间的赋值,不执行拷贝构造函数。
但是要注意,对于情况三的第2、3种,我这样写只能打印一次,因为说过很多遍了其在栈区用完就清理了,得到的地址没变,里面的值没了。
People* foo() {
People p(99);
cout << (int*)&p << endl;
cout << (int)&p << endl;
return &p;
}
int main() {
People* p = foo();
cout << (int*)&*p << endl;
cout << (int)&p << endl;
}
/*
普通:有参构造
00BBFAC8
12319432
拜拜
00BBFAC8
12319672
*/
当然你也可以new一块或者static,不过学指针也引用的时候我也写了,不提倡。
所以return回一个对象结构体啥的,因为可能还要用到就别整花里胡哨的直接返回个值就行,当作参数传递给函数可以考虑指针引用节省空间,可以考虑加个const不让他变。
如果这些你都想到了,说明你前面没白学。
构造函数的调用规则
默认情况下,C++编译器至少给一个类添加三个函数
默认构造函数(无参、函数体为空)
默认析构函数(无参、函数体为空)
默认拷贝函数构造函数,对全部属性的值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
即:
用户提供了有参,编译器不会提供无参,但会提供拷贝;
用户提供了拷贝,编译器什么构造函数都不会提供。
深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作。
- 深拷贝:在堆区中重新申请空间,进行拷贝操作。
#include<iostream>
using namespace std;
class Person
{
public:
int m_Age;
int* m_Height;
Person()
{
cout << "Person的默认构造函数调用" << endl;
}
Person(int age,int height)
{
m_Height = new int(height); //把身高开辟到堆区
m_Age = age;
cout << "Person的有参构造函数调用" << endl;
}
Person(const Person& p)
{
cout << "Person的拷贝构造函数调用" << endl;
m_Age = p.m_Age;
m_Height = p.m_Height; //编译器默认实现的就是这行代码
}
~Person()
{
//将堆区开辟的数据进行释放
if (m_Height !=NULL)
{
delete m_Height;
m_Height = NULL;
}
cout << "Person的析构构造函数调用" << endl;
}
};
void foo()
{
Person p1(18,166);
cout << p1.m_Age<<" " << *p1.m_Height << endl;
Person p2(p1);
cout << p2.m_Age<<" " <<*p2.m_Height<< endl;
}
int main(void)
{
foo();
return 0;
}
此时这样执行会发生报错,内存重复释放。
解决:编译器提供的浅拷贝不行了,我们自己深拷贝重新开辟一块空间:
Person(const Person& p)
{
cout << "Person的拷贝构造函数调用" << endl;
m_Age = p.m_Age;
//m_Height = p.m_Height; //编译器默认实现的就是这行代码
m_Height = new int(*p.m_Height);
}
结果:
/*
Person的有参构造函数调用
18 166
Person的拷贝构造函数调用
18 166
Person的析构构造函数调用
Person的析构构造函数调用
*/
即:如果有属性在堆区开辟的,一定要自己提供拷贝构造函数新开辟一块空间,防止浅拷贝在析构函数释放内存时带来的问题。
初始化列表
了解一下就行。
作用:
C++提供了初始化列表语法,用来初始化对象。
语法:
构造函数():属性1(值1),属性2(值2)…{}
示例:
#include<iostream>
using namespace std;
class Person
{
public:
//传统赋值操作
/*Person(int a, int b, int c)
{
m_A = a;
m_B = b;
m_C = c;
}*/
//初始化列表初始化属性
Person(int a,int b,int c) :m_A(a), m_B(b), m_C(c){}
int m_A;
int m_B;
int m_C;
};
void test()
{
//Person p(10,20,30);
Person p(30,20,10);
cout << p.m_A << endl;
cout << p.m_B << endl;
cout << p.m_C << endl;
}
int main(void)
{
test();
system("pause");
return 0;
}
类对象作为类成员
C++中类的成员可以是另一个类的对象,我们称该成员为对象成员。
如:
B类中有对象A作为成员,A为对象成员。
那么当创建B对时,A先被构造,当其他类的对象作为本类的成员时,构造时先构造其他类的对象,再构造自身。
析构则与构造函数相反。自身的析构函数先进行,之后其它类再进行。
#include<iostream>
#include<string.h>
using namespace std;
class Student{
public:
int id;
string name;
Student() {};
Student(int i, string n) {
id = i;
name = n;
}
};
class Teacher {
public:
int id;
string name;
Student s;
Teacher(int i, string n, Student stu) {
id = i;
name = n;
s = stu;
}
};
int main(void)
{
Student s(001, "张三");
Teacher t(001, "李老师", s);
cout << t.s.name << endl; // 张三
return 0;
}
Teacher(int i, string n, Student stu) 这行要求Student有默认的无参构造函数,因为我写了有参的,系统不再提供无参,所以要自己手动加一个。当然如果Teacher(int i, string n, Student stu = Student(007, "张三sss")) 弄个默认的就不用。
#include<iostream>
#include<string.h>
using namespace std;
class Student{
public:
int id;
string name;
Student() {};
Student(int i, string n) {
id = i;
name = n;
}
};
class Teacher {
public:
int id;
string name;
Student s;
Teacher(int i, string n, Student stu = Student (001, "张三")) {
id = i;
name = n;
s = stu;
}
};
int main(void)
{
Student s(001, "张三");
Teacher t1(001, "李老师", s);
Teacher t2(001, "王老师");
cout << t1.s.name << endl; // 张三
cout << t2.s.name << endl; // 张三
return 0;
}
此时在Teacher(int i, string n, Student stu = Student (001, "张三")) {接收时,不能写Student stu(001, "张三") 而是Student stu = Student (001, "张三")。
当然也可以用隐式转换法去比如用一个字符串赋值给对象将它进行实例化。
静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员。
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象成员共享同一个函数
- 静态成员函数只能访问静态成员变量,非静态无法访问。
class Person {
public:
static void func() {
cout << "ststic 函数调用" << endl;
}
};
int main(void)
{
// 通过对象访问
Person p;
p.func(); // ststic 函数调用
// 通过类访问
Person::func(); // ststic 函数调用
// 通过类名访问,说明不属于某个对象。参考Python的类成员。
return 0;
}
对于静态成员变量,类内声明类外初始化。非静态的其实可以类内初始化;
#include<iostream>
#include<string.h>
using namespace std;
class Person {
public:
int b = 20; // 就相当于默认值了,愿意拷贝构造重新赋值就重新赋,愿意改就改。
static int a;
// static int a = 100; 飘红
static void func() {
a = 99;
}
};
int Person::a=20; // 不写的话下面p.a引发报错。
int main(void)
{
Person p;
cout << p.b << endl; // 1
return 0;
}
静态成员函数访问非静态成员变量
class Person {
public:
static int a; // 类内声明 类外初始化
static void func() {
// x = 0; 非静态成员变量无法访问
a = 99; // 静态成员函数可以访问静态成员变量
cout << "ststic 函数调用" << endl;
}
void xx() {
a = 33;
}
};
int Person:: a = 100;// 类内声明 类外初始化
int main(void)
{
Person p;
cout << p.a << endl; // 100
p.func();
cout << p.a << endl; // 99
p.xx();
cout << p.a << endl; // 33
return 0;
}
为什么静态成员函数只能访问静态成员变量,非静态无法访问?
我们上面知道该static静态方法是类成员,对于非静态成员只有进行实例化对象的时候才能对其进行赋值或者调用。
假如你有p1,p2两个对象,调用这个静态函数让他去改变非静态成员变量,他自己也不知道该改变谁的非静态变量。Person::func(); p1.func(); p2.func();
当然如果静态成员函数是私有的,类外一样访问不到。
C++对象模型
在C++中,类内的成员变量和成员函数分开存储,
只有非静态成员变量才属于类的对象上。
(只有非静态成员变量的大小算进类的大小中,其他的都不算。)
C++空对象的大小是1,为的是区分不同类在内存中的占用位置。
每个空对象也应该有一个独一无二的字节。
#include<iostream>
#include<string.h>
using namespace std;
class Person {
};
int main(void)
{
Person p;
cout << sizeof(p) << endl; // 1
return 0;
}
只有非静态变量时
class Person {
int a;
float b;
};
int main(void)
{
Person p;
cout << sizeof(p) << endl; // 4+4=8
return 0;
}
只有非静态函数时
class Person {
void func() {
int a = 99;
}
int func1() {
int b = 20;
return 10;
}
};
int main(void)
{
Person p;
cout << sizeof(p) << endl; // 1
return 0;
}
有静态成员
class Person {
static int a;
static void func() {
a = 99;
}
};
int Person::a=20;
int main(void)
{
Person p;
cout << sizeof(p) << endl; // 1
return 0;
}
即 在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
this指针
我们上面知道C++成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码。
那么问题是:这一块代码是如何区分是哪个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。(这里的this指针可以参考JavaScript。)
this指针指向被调用的成员函数所属的对象。谁调的,this就指向谁。
this指针是隐含每个非静态成员函数内的一种指针。
this指针的用途
当形参和成员变量同名时,可用this指针来区分(这也是为什么我说不像python一样能起同样的名字赋值)
在类的非静态成员函数中返回对象本身,可使用return *this
解决名称冲突
#include<iostream>
#include<string.h>
using namespace std;
class Person {
public:
int age;
Person(int age) {
this->age = age;
}
Person(const Person &p) {
age = p.age;
cout << "拷贝构造函数" << endl;
}
Person func() {
return *this;
}
};
int main(void)
{
Person p(99); // 这里不可写Person p; 默认构造函数已经无了。
cout << p.age << endl; // 99
Person p1 = p.func(); // 复习:这里其实又调用了拷贝构造函数
cout << p1.age << endl; // 99
return 0;
}
cout << this << endl;是一块地址 *this是调用的对象
Person(int age) {
this->age = age;
// *this->age = age; 飘红
}
this指针的本质是指针常量,指针的指向是不可以修改的 Person *const this;
复习:拷贝构造函数(看好是哪步调用的)
Person func() {
return *this;
}
};
int main(void)
{
Person p(99);
cout << p.age << endl; // 99
p.func(); // 调用拷贝构造函数 生成这里的匿名对象 直接释放
return 0;
}
Person& func() {
return *this;
}
};
int main(void)
{
Person p(99);
cout << p.age << endl;
p.func(); // 不调用拷贝构造函数
return 0;
}
Person& func() {
return *this;
}
};
int main(void)
{
Person p(99);
cout << p.age << endl;
Person p1 = p.func();
cout << p1.age << endl; // 调用拷贝构造函数 此时没有用与函数匹配的&去接收
return 0;
}
Person& func() {
return *this;
}
};
int main(void)
{
Person p(99);
cout << p.age << endl;
Person &p1 = p.func();
cout << p1.age << endl; // 不调用拷贝构造函数
return 0;
}
Person func() {
return *this;
}
};
int main(void)
{
Person p(99);
cout << p.age << endl;
Person p1 = p.func(); // 匿名对象赋值给p1只调用一次拷贝构造函数
cout << p1.age << endl;
return 0;
}
空指针调用成员函数
#include<iostream>
using namespace std;
class Person
{
public:
void ShowClassName()
{
cout << "this is Person class" << endl;
}
void ShowPersonAge()
{
//提高健壮性,空的就直接返回,防止代码崩溃
if (this == NULL)
{
return;
}
//报错原因是因为传入的指针是NULL——无中生有,用一个空指针访问里面的属性
cout << this->m_Age << endl;
}
int m_Age;
};
void test()
{
Person* p = NULL;
p->ShowClassName();
p->ShowPersonAge();
}
int main(void)
{
test();
system("pause");
return 0;
}
const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前const称该对象为常对象。
- 常对象只能调用常函数。
#include<iostream>
using namespace std;
class Person{
public:
//this指针的本质是指针常量,指针的指向是不可以修改的
//就相当于Person *const this;
//在成员函数后面加const修饰的是this指向,让指针指向的值也不可以修改
void showPerson() const//加个const就不允许修改了 const Person *const this;
{
this->m_b = 100;
//this = NULL;tbhis指针是不可以修改指针的指向的
}
int m_a;
mutable int m_b;//加了mutable修饰的特殊变量,即使在常函数,常对象中,也可以修改这个值
void func()
{
m_a = 100;//在普通成员函数中是可以修改的
}
};
void test()
{
Person P;
P.showPerson();
}
//常对象
void test1()
{
const Person p;//在对象前加const,变为常对象
//p.m_a = 100; 不可改
p.m_b = 100;
//常对象只能调用常函数
p.showPerson();
//p.func();常对象不能调用普通成员函数,因为普通成员函数可以修改属性。
}
int main(void)
{
test();
system("pause");
return 0;
}
友元
在程序中,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元技术。
友元的目的就是让一个函数或者类 访问另一个类中的私有元素。
友元的关键字 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
#include<iostream>
#include<string>
using namespace std;
class Building
{
//goodgay全局函数是Building类的一个好朋友,可以访问私有成员。
friend void goodgay(Building* building);
public:
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//全局函数
void goodgay(Building* building) // 这不为了前面讲的节省空间吗
{
cout << "好基友全局函数正在访问你的" << building->m_SittingRoom << endl;
cout << "好基友全局函数正在访问你的" << building->m_BedRoom << endl;
}
void test()
{
Building building;
goodgay(&building);
}
int main(void)
{
test();
system("pause");
return 0;
}
类做友元
#include<iostream>
#include<string>
using namespace std;
//在前面先声明一下
class Building;
class GoodGay
{
public:
GoodGay();
public:
void visit();//参观函数 访问Building中的属性
Building* building;
};
class Building
{
//GoodGay是Building类的好朋友,可以访问其私有属性
friend class GoodGay;
public:
Building();
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
//在类外写成员函数
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
//创建一个Building对象
building = new Building;
}
void GoodGay::visit()
{
cout << "好基友正在访问你的" << building->m_SittingRoom << endl;
cout << "好基友正在访问你的" << building->m_BedRoom << endl;
}
void test()
{
GoodGay gy;
gy.visit();
}
int main(void)
{
test();
system("pause");
return 0;
}
成员函数做友元
#include<iostream>
#include<string>
using namespace std;
class Building;
class GoodGay
{
public:
GoodGay();
void visit();//可以访问Building中私有成员
void visit1();//不可以访问Building中私有成员
Building* builidng;
};
class Building
{
//告诉编译器 GoodGay类中的visit成员函数作为本类的好朋友,可以访问私有函数
friend void GoodGay::visit();
public:
Building();
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
Building::Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
builidng = new Building;
}
void GoodGay::visit()
{
cout << "visit正在访问" << builidng->m_SittingRoom << endl;
cout << "visit正在访问" << builidng->m_BedRoom << endl;
}
void GoodGay::visit1()
{
cout << "visit1正在访问" << builidng->m_SittingRoom << endl;
}
void test()
{
GoodGay gg;
gg.visit();
gg.visit1();
}
int main(void)
{
test();
system("pause");
return 0;
}
继承
class Person {
// 父类
};
class boy :public Person {
// 子类
};
public 类内可以访问 类外可以访问 子类可以访问
private 类内可以访问 类外不可以访问 子类不可以访问
protected 类内可以访问 类外不可以访问 子类可以访问
父类中所有的非静态成员属性都会被子类继承下去。
父类中私有的成员属性是被编译器给隐藏了,因此访问不到,但是确实被继承下去了
继承中构造和析构的顺序
子类继承父类后,当创建子类时,也会调用父类的构造函数。
继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和类名)。
同名访问:父类加作用域
//同名成员属性处理方式
void test01()
{
Son son;
cout <<son.m_A<< endl;
//如果要通过子类对象访问到父类中的同名成员,需要加作用域。
cout <<son.Base::m_A<< endl;
}
//同名成员函数处理方式
void test02()
{
Son son1;
son1.func();//子
son1.Base::func();//父
//如果子类中出现和父类同名的成员函数
//子类的同名成员会隐藏掉父类中所有同名成员函数
//如果想要访问到父类中被隐藏的同名成员函数,需要加作用域
son1.Base::func(10);
}
int main(void)
{
test02();
system("pause");
return 0;
}
多态
多态分为两种
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include<iostream>
using namespace std;
class Animal
{
public:
//加上virtual变成虚函数,实现地址晚绑定
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//地址早绑定,在编译阶段就确定函数地址
//如果想让猫说话,那么这个函数的地址就不能提前绑定,需要在运行阶段进行绑定
//动态多条满足条件
/*
1.有继承关系
2.子类重写父类的虚函数
*/
//重写要求:函数返回值类型 函数名 参数列表 完全相同
//动态多态的使用
/*
父类的指针或者引用 指向子类的对象//Animal &animal = cat;
*/
void doSpeak(Animal& animal)//Animal &animal = cat;
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main(void)
{
test01();
system("pause");
return 0;
}
假如我不加virtual,则cout出的结果只有:动物在说话
原理
我们打印下Animal实例化对象的大小:
int main(void)
{
Animal a;
cout << sizeof(a) << endl; // 4
return 0;
}
输出结果为4,当然如果你选择x64架构结果应该为8。
我们知道正常一个空对象其大小为1,这里的4或8实际上为一个指针的大小。它多了个vfptr,虚函数(表)指针。
简单说下就是由于子类继承父类的时候,继承父类的全部,所以因为父类写了virtual,多了个vfptr指针,继承下来的时候指针指向自己了如猫类狗类,所以在void doSpeak(Animal& animal)接收一个猫或者狗对象的时候指针就会指向它自己,而不再是父类Animal。
纯虚函数和抽象类
在多态中,通常父类汇中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include<iostream>
using namespace std;
class Animal
{
public:
//加上virtual变成虚函数,实现地址晚绑定
virtual void speak() = 0;
};
class Cat :public Animal
{
public:
virtual void speak() // 要加上这个
{
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal
{
public:
virtual void speak() // 要加上这个
{
cout << "小狗在说话" << endl;
}
};
void doSpeak(Animal& animal)
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main(void)
{
test01();
return 0;
}
此时如果执行 Animal a;报错,抽象类无法实例化对象。
虚析构和纯虚析构
问题:多态使用的时候,如果子类中有属性开辟到堆区,那么父类指针在释放的时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯析构作用:
- 可以解决父类指针释放子类对象,
- 都需要有具体的含函数实现
虚析构和纯虚构的区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法;
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;//声明
类名::~类名(){}
#include<iostream>
#include<string>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal的构造函数调用" << endl;
}
//利用虚析构可以解决父类指针释放对象时不干净的问题
/*virtual ~Animal()
{
cout << "Animal的析构函数调用" << endl;
}*/
//纯虚析构,需要声明也需要实现
//有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
virtual ~Animal() = 0;
virtual void speak() = 0;
};
//纯虚析构函数
Animal::~Animal()
{
cout << "Animal纯析构函数调用" << endl;
}
class Cat :public Animal
{
public:
Cat(string name)
{
m_Name = new string(name);
}
virtual void speak()
{
cout << "Cat的构造函数调用" << endl;
cout << *m_Name << "小猫在说话" << endl;
}
~Cat()
{
if (m_Name != NULL)
{
cout << "Cat的析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
string* m_Name;
};
void test01()
{
Animal* animal = new Cat("Tom");
/*
父类的指针在析构的时候,不会调用子类中的析构函数,
导致子类如果有堆区属性,会出现内存的泄漏情况。
解决:将父类的析构函数改为虚析构
*/
animal->speak();
delete animal; // 这个要加 不然看不见打印出来的析构函数
}
int main(void)
{
test01();
return 0;
}
/*
Animal的构造函数调用
Cat的构造函数调用
Tom小猫在说话
Cat的析构函数调用
Animal纯析构函数调用
*/
这里我们猫类中string* m_Name接收了new string(name),所以需要释放,正常情况下子类的析构函数是可以走的,但此时声明对象时父类指针在堆区,delete它自己影响不到子类。
ps:以下不写虚析构都能正常释放
void test01()
{
Cat c("Tom");
Animal &animal = c; // 这种写法Animal &animal = Cat("Tom")报错
animal.speak();
/*
Cat c("Tom");
Animal* animal = &c;
animal->speak();
*/
}
int main(void)
{
test01();
return 0;
}
提问:Animal* animal = &Cat("Tom");这样写可以吗? 答不可以。
此外 子类中有this,父类指针也无法指向自身。