C++笔记-2
关于extern的用法:
利用关键字extern,可以在一个文件中引用另一个文件中定义的变量或者函数,下面就结合具体的实例,分类说明一下。
一、引用同一个文件中的变量1 |
|
如果按照这个顺序,变量 num在main函数的后边进行声明和初始化的话,那么在main函数中是不能直接引用num这个变量的,因为当编译器编译到这一句话的时候,找不到num这个变量的声明,但是在func函数中是可以正常使用,因为func对num的调用是发生在num的声明和初始化之后。
如果我不想改变num的声明的位置,但是想在main函数中直接使用num这个变量,怎么办呢?可以使用extern这个关键字。像下面这一段代码,利用extern关键字先声明一下num变量,告诉编译器num这个变量是存在的,但是不是在这之前声明的,你到别的地方找找吧,果然,这样就可以顺利通过编译啦。但是你要是想欺骗编译器也是不行的,比如你声明了extern int num;但是在后面却没有真正的给出num变量的声明,那么编译器去别的地方找了,但是没找到还是不行的。
下面的程序就是利用extern关键字,使用在后边定义的变量。
1 |
|
如果extern这个关键字就这点功能,那么这个关键字就显得多余了,因为上边的程序可以通过将num变量在main函数的上边声明,使得在main函数中也可以使用。
extern这个关键字的真正的作用是引用不在同一个文件中的全局变量或者全局函数。如果将全局变量定义在b.c中,当其他的.cpp文件想要使用该全局变量,#include “包含全局变量的源文件对应的头文件”是无法将其调用过来的。而如果定义在b.h中,则其他源文件#include “b.h”就可以使用全局变量了,但你会说那我把全局变量都定义在.h文件中,其他源文件用的时候#include一下就可以了,根本不需要extern了,extern就没有用了啊!
- 这将导致每个包含该头文件的源文件都会生成一个独立的全局变量 num 的副本。这可能会在链接时引发多个重复定义错误。
- 一个project会有很多全局变量,这些全局变量的定义一般都放在一个.h文件中,如果你只想使用num这个全局变量,那么每一个使用num的.cpp文件都需要#include “全局变量头文件”,编译器在编译时会给其他全局变量在全局区创造副本,但你并没有使用它们,这样极其耗费资源。
以下面的例子来说,想要调用b.c文件中的全局变量num,出现了以下两种方法: - 在main.cpp中不#include “b.h”,而是加入一行代码 extern int num;可以把这行代码放在main函数中,也可以放在全局区中,也可以放在main.h中。编译器看到extern关键字就会在整个project目录下搜索。见例子一
- 在b.h中加入extern int num,再在main.c中 #include “b.h”。见例子二
例子一:
1 |
|
1 |
|
例子二:
b.h1 |
|
1 |
|
1 |
|
例如,这里b.c中定义了一个变量num,如果main.c中想要引用这个变量,那么可以使用extern这个关键字,注意这里能成功引用的原因是,num这个关键字在b.c中是一个全局变量,也就是说只有当一个变量是一个全局变量时,extern变量才会起作用,像下面这样num是另一个源文件的局部变量是不行的。
mian.c1 |
|
另外,extern关键字只需要指明类型和变量名就行了,不能再重新赋值,初始化需要在原文件所在处进行,如果不进行初始化的话,全局变量会被编译器自动初始化为0。像这种写法是不行的。
extern int num=4;
但是在声明之后就可以使用变量名进行修改了,像这样:1
2
3
4
5
6
7
8
int main()
{
extern int num;
num=1;
printf("%d",num);
return 0;
}
如果不想这个变量被修改可以使用const关键字进行修饰,写法如下:
1 |
|
1 |
|
使用include将另一个文件全部包含进去可以引用另一个文件中的变量,但是这样做的结果就是,被包含的文件中的所有的变量和方法都可以被这个文件使用,这样就变得不安全,如果只是希望一个文件使用另一个文件中的某个变量还是使用extern关键字更好。
三、引用另一文件中的函数extern除了引用另一个文件中的变量外,还可以引用另一个文件中的函数,引用方法和引用变量相似。
mian.c1 |
|
这里面隐藏着一个很严重的错误:当veci.erase(iter)之后,iter就变成了一个野指针,对一个野指针进行 iter++ 是肯定会出错的。
我们通过查阅文档可以看到erase函数的返回值是这么介绍的:一个迭代器,指定在任何删除的元素之后剩余的第一个元素,如果不存在这样的元素,则指定指向向量结尾的指针。并且比如vector里有4个int型,内存从xxxxx10到xxxxx20,4*4字节=16,这是vector容器的大小。eraser()删除了一个元素,后面的元素补上来,iter指向的内存被释放掉,iter变为野指针,对野指针做任何操作都会报错,因此要重新赋值。这里需要注意,系统给vector初始化的空间是不会变的,删完数据后vector.end()指向的不是初始化的结尾,是实际存储变量的下一个字节。
按如下修改:1
2
3
4
5for(auto iter=vec.begin();iter!=vec.end(); iter++)
{
if(*iter == 3)
iter = veci.erase(iter);
}
但是这种代码也是存在缺陷的,首先是我们无法连续删除数字3,其次是迭代器在指向vec.end()的时候,还会进行一次++,这就发生了数组越界,所以我们一概这样修改:1
2
3
4
5
6
7
8for(auto iter=vec.begin();iter!=vec.end(); )
{
if( *iter == 3)
iter = veci.erase(iter);//当删除时erase函数自动指向下一个位置,就不需要进行++
else
iter ++ ; //当没有进行删除的时候,迭代器++
}
//此时vec.end()指向的内存是删完后的int型的下一个字节。因此这时cout<<*vec.end();会报错
图文讲解:
文
https://blog.csdn.net/Vcrossover/article/details/106243627
图
https://www.cnblogs.com/chaohacker/p/13024357.html
typedef
C语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。
起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是 stu,要想定义一个结构体变量就得这样写:1
struct stu stu1;
struct 看起来就是多余的,但不写又会报错。如果为 struct stu 起了一个别名 STU,书写起来就简单了:1
STU stu1;
使用关键字 typedef 可以为类型起一个新的别名。typedef 的用法一般为:1
typedef oldName newName;
oldName 是类型原来的名字,newName 是类型新的名字。例如:1
2
3
4typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;
INTEGER a, b;等效于int a, b;。
数组字符串
typedef 还可以给数组、指针、结构体等类型定义别名。先来看一个给数组类型定义别名的例子:1
typedef char ARRAY20[20];
表示 ARRAY20 是类型char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:
ARRAY20 a1, a2, s1, s2;
它等价于:1
char a1[20], a2[20], s1[20], s2[20];
结构体
又如,为结构体类型定义别名:1
2
3
4
5 typedef struct stu{
char name[20];
int age;
char sex;
} STU;
STU 是 struct stu 的别名,可以用 STU 定义结构体变量:1
STU body1,body2;
它等价于:1
struct stu body1, body2;
指针
再如,为指针类型定义别名:1
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:1
PTR_TO_ARR p1, p2;
关于宏定义
宏定义在预编译阶段就会被替换掉。当编译器不知道宏表示的内容时默认为0。
typedef 和 宏定义define 的区别
typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的类型,可以把int这种不属于类型的(int,double是类型)定义为类型。
define是类似于“文本替换”的意思,把···替换成···,#define 定义一个标识符来表示一个常量,其定义的常量值没有类型限定,也不做类型检查(下面由于int本来就不是一个类型,所以出现问题),在出现宏名称的地方直接展开。其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。即其在预编译过程中就已经被全部替换掉了,而不需要将其加入到符号表中占用内存。
例如可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:1
2
3
4
5
unsigned INTERGE n; //没问题
typedef int INTERGE;
unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
可以看到变量b的类型是int,其他三个是int 类型。这是因为#define是查找替换,所以替换过后的语句是“inta, b;”,在C语言中,指针并不是一个type(类型),只有type才能连续定义(比如int c,d)。而typedef是类型转换。这就不难看出b为什么是一个int类型变量,如果要让b也是指针,必须写成“int a, b;”。而我们使用typedef时不会出现这个问题,可以看到c、d都是整型指针。
关于enum和#define(effective C++ P15)
1 |
|
1 |
|
猜,答案会是多少呢?
1 |
|
在预编译阶段,编译器会发现 #if (b==0),此时由于这行代码是宏定义,不知道表达式中的b是多少,编译器直接会用0替代(预编译过程中仅仅做的是展开#define,#include,处理条件编译#if,#else,删除注释,添加行号和文件标识符等,并不会执行#if条件下的代码)。因此会如此输出。此外enum中如果没有赋值,会把第一个元素默认为0,往后逐渐加一。
Union(联合)
用法
1 | union Data { |
这段代码定义了一个名为data的union变量。它有什么属性呢?
- 这个变量在内存中占用4个字节的空间而不是8个;
- 有两个数据成员:int类型变量的i和char类型的数组str;
- 虽然有两个数据成员,但是这两个成员对应的存储空间是同一块内存。也就是说定义了char str[4],访问 i ,得到的结果就是定义了的char str[4]
上面三点是union变量的最基本也是最重要的属性。详细说一下第三点。因为union不论包含多少个多少种数据类型,它实例化为变量后,这个变量的长度是这个union中最长的数据类型的长度。下面的代码定义了一个union变量。它的长度是16个字节。1
2
3
4
5union DEMO{
char status;
int a;
int serial[4];
}demo;
应用
C语言中,union相对于struct使用的次数在大部分项目中都处于明显的劣势,这和union的存储方式的特性有很大的关系。在union中,所有的字段都有相同的偏移量,而且所有的字段都是相互重叠的,union的大小是其中最大字段的大小。那我们就知道,如果所有的字段是相互重叠的,那改变其中任何一个字段的值,其他字段的值都会受到影响,也会发生变化。这就造成union在实际使用中使用的频率不会那么高,甚至会认为可能也没有什么用。如果想要使用的话,那么union中的各个字段的使用必须是互斥的,任意时刻只能使用一个。
- 判断大小端,union大显身手
一个整数在大小端机器上面存储的顺序是不一样,而union中的各个字段的偏移地址是相同的,那一个数在在大小端机器中存储到union中,如果将这个数拆分,各个部分也会不同。1
2
3
4
5
6
7
8
9
10
11
12
13typedef union {
unsigned long bits32;
unsigned char bytes[4];
} TheValue;
TheValue theValue;
int isLittleEndian = 0;
theValue.bytes[0] = 0;
theValue.bytes[1] = 1;
theValue.bytes[2] = 0;
theValue.bytes[3] = 0;
isLittleEndian = (theValue.bits32 == 256);
#256小端法16进制:00 01 00 00
#256大端法16进制:00 00 01 00 - 创建别名
因为程序中经常会进行类型的强制转换,如果不小心可能就会出错,那么我们就可以利用union中的字段代表想要得到的类型,尤其是指针类型,尤其是代码整合过程中,如果使用了第三方的库,需要将第三方的库merge到自己的代码中,由于编码习惯,命名规则的不同,还是需要将其他库的一些类型转换为自己习惯的方式或者公司的方式。一般情况我们是能看到库的header file的,结构类型什么的都可以看到。我们会按照库的header file写一份自己的。
例如库中header file有一个struct 名字是ThirdTest
那么我们在header file中创造一个一个对应的struct 名字是OurTestThird
那么就可以弄一个union那我们使用一下1
2
3
4typedef union {
ThirdTest * thirdTest;
OurTestThird *ourTestThird;
} TestThird;那这个union TestThird就起到了将类型ThirdTest 强转为OurTestThird 的作用,union起到了一个桥梁的作用和粘合剂的作用,不然就需要一个表将自己的类型和库的类型一一对应起来。1
2
3TestThird testThird;
testThird.thirdTest = getCallOneFunction(); //这个一个库函数,返回的类型是ThirdTest *
CallOurSomeOneFunction(testThird. ourTestThird); //这个是自己的函数,参数类型是OurTestThird *
当然这个例子有些牵强,有很多办法将类型对应起来,这里只是针对别名列举一个例子而已。 - 将union中较大的对象分解成组成这个对象的各个字节。
1
2
3
4
5
6
7typedef union {
unsiged int u;
unsiged char bytes[4];
} asBytes;
asBytes composite;
composite.u = 1234576890;
printf (“HO byte of composite.u is %u, LO byte is %u\n”, composite.bytes[3], composite.bytes[0]);
枚举enum
关键字enum的定义
enum是C语言中的一个关键字,enum叫枚举数据类型,枚举数据类型描述的是一组整型值的集合(这句话其实不太妥当),枚举型是预处理指令#define的替代,枚举和宏其实非常类似,宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值,
我们可以将枚举理解为编译阶段的宏,使用格式:1
enum typeName { valueName1, valueName2, valueName3, ...... };
typeName是枚举类型的名字,花括号里面的元素(枚举成员)是常量而不是变量,这个一定要搞清楚,因为枚举成员的是常量,所以不能对它们赋值,只能将它们的值赋给其他的变量。
枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。
接下来我们举个例子,比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:1
2
3
4
5
6
7#defineMON 1
#defineTUE 2
#defineWED 3
#defineTHU 4
#defineFRI 5
#defineSAT 6
#defineSUN 7
这个看起来代码量就比较多,接下来我们看看使用枚举的方式:1
2
3
4enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
这样看起来是不是更简洁了。
需要注意的是
- 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
- Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。 - 第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1。在当前值没有赋值的情况下,枚举类型的当前值总是前一个值+1.
- 枚举的每个常量都是按照整数int类型来存储的。
枚举变量的定义
前面我们只是声明了枚举类型,接下来我们看看如何定义枚举变量。
我们可以通过以下三种方式来定义枚举变量
- 先定义枚举类型,再定义枚举变量:
1
2
3
4
5enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day; - 定义枚举类型的同时定义枚举变量
1
2
3
4enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day; - 省略枚举名称,直接定义枚举变量同一个程序中不能定义同名的枚举类型,不同的枚举类型中也不能存在同名的命名常量。错误示例如下所示:
1
2
3
4enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day; - 存在同名的枚举类型
1
2
3
4
5
6
7
8
9
10
11
12typedef enum
{
wednesday,
thursday,
friday
} workday;
typedef enum WEEK
{
saturday,
sunday = 0,
monday,
} workday; - 存在同名的枚举成员
1
2
3
4
5
6
7
8
9
10
11
12typedef enum
{
wednesday,
thursday,
friday
} workday_1;
typedef enum WEEK
{
wednesday,
sunday = 0,
monday,
} workday_2;使用枚举类型的变量
对枚举型的变量赋值
实例将枚举类型的赋值与基本数据类型的赋值进行了对比: - 先声明变量,再对变量赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 定义枚举类型 */
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN };
void main()
{
/* 使用基本数据类型声明变量,然后对变量赋值 */
int x, y, z;
x = 10;
y = 20;
z = 30;
/* 使用枚举类型声明变量,再对枚举型变量赋值 */
enum DAY yesterday, today, tomorrow;
yesterday = MON;
today = TUE;
tomorrow = WED;
printf("%d %d %d \n", yesterday, today, tomorrow);
}
```
2. 声明变量的同时赋初值
```c
/* 定义枚举类型 */
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN };
void main()
{
/* 使用基本数据类型声明变量同时对变量赋初值 */
int x=10, y=20, z=30;
/* 使用枚举类型声明变量同时对枚举型变量赋初值 */
enum DAY yesterday = MON,
today = TUE,
tomorrow = WED;
printf("%d %d %d \n", yesterday, today, tomorrow);
} - 定义类型的同时声明变量,然后对变量赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 定义枚举类型,同时声明该类型的三个变量,它们都为全局变量 */
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN } yesterday, today, tomorrow;
/* 定义三个具有基本数据类型的变量,它们都为全局变量 */
int x, y, z;
void main()
{
/* 对基本数据类型的变量赋值 */
x = 10; y = 20; z = 30;
/* 对枚举型的变量赋值 */
yesterday = MON;
today = TUE;
tomorrow = WED;
printf("%d %d %d \n", x, y, z); //输出:10 20 30
printf("%d %d %d \n", yesterday, today, tomorrow); //输出:1 2 3
} - 类型定义,变量声明,赋初值同时进行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 定义枚举类型,同时声明该类型的三个变量,并赋初值。它们都为全局变量 */
enum DAY
{
MON=1,
TUE,
WED,
THU,
FRI,
SAT,
SUN
}
yesterday = MON, today = TUE, tomorrow = WED;
/* 定义三个具有基本数据类型的变量,并赋初值。它们都为全局变量 */
int x = 10, y = 20, z = 30;
void main()
{
printf("%d %d %d \n", x, y, z); //输出:10 20 30
printf("%d %d %d \n", yesterday, today, tomorrow); //输出:1 2 3
} - 注意:给枚举变量赋予初值后再次赋值
1
2
3
4
5
6
7
8
9
10
11
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN };
void main()
{
enum DAY yesterday, today, tomorrow;
yesterday = TUE;
today = (enum DAY) (yesterday + 1); //类型转换
tomorrow = (enum DAY) 30; //类型转换
//tomorrow = 3; //错误
printf("%d %d %d \n", yesterday, today, tomorrow); //输出:2 3 30
}枚举与#define 宏的区别
下面再看看枚举与#define 宏的区别: - define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值(在预编译阶段使用enum值会出错,因为在编译阶段才确定其值)。
- 一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
- 枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。
malloc函数与calloc函数
malloc
malloc函数原型
1 | extern void *malloc(unsigned int num_bytes); |
意为分配长度为num_bytes字节的内存块
malloc函数头文件
1 |
malloc函数返回值
如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
malloc函数使用注意事项
- malloc函数的返回的是无类型指针,在使用时一定要强制转换为所需要的类型。
- 重点:在使用malloc开辟空间时,使用完成一定要释放空间,如果不释放会造内存泄漏。
- 在使用malloc函数开辟的空间中,不要进行指针的移动,因为一旦移动之后可能出现申请的空间和释放空间大小的不匹配
malloc使用
关于malloc所开辟空间类型:malloc只开辟空间,不进行类型检查,只是在使用的时候进行类型的强转。举个例子:‘我’开辟你所需要大小的字节大小空间,至于怎么使用是你的事
mallo函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用
指针自身 = (指针类型 ) malloc(sizeof(指针类型)数据数量)在使用malloc函数之前我们一定要计算字节数,malloc开辟的是用户所需求的字节数大小的空间。1
2
3int *p = NULL;
int n = 10;
p = (int *)malloc(sizeof(int)*n);
如果多次申请空间那么系统是如何做到空间的不重复使用呢?
在使用malloc开辟一段空间之后,系统会在这段空间之前做一个标记(0或1),当malloc函数开辟空间如果遇到标记为0就在此开辟,如果为1说明此空间正在被使用。释放函数free()
作用:释放malloc(或calloc、realloc)函数给指针变量分配的内存空间。
注意:使用后该指针变量一定要重新指向NULL,防止悬空指针(失效指针)出现,有效规避错误操作。free函数在释放空间之后,把内存前的标志变为0,且为了防止数据泄露,它会把所释放的空间用cd进行填充。1
2
3
4
5
6
7
8
9int main()
{
int *p = (int *)malloc(sizeof(int));
*p = 100;
free(p);
p = NULL;
return 0;
}
calloc
calloc函数,其原型void calloc(size_t n, size_t size);
其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,会int p = (int *)calloc(20, sizeof(int)),这样就省去了人为空间计算的麻烦。但这并不是他们之间最重要的区别,malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0;
calloc与malloc区别
- 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题.
- 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零.
疑问:既然calloc不需要计算空间并且可以直接初始化内存避免错误,那为什么不直接使用calloc函数,那要malloc要什么用呢?
实际上,任何事物都有两面性,有好的一面,必然存在不好的地方。这就是效率。calloc函数由于给每一个空间都要初始化值,那必然效率较malloc要低,并且现实世界,很多情况的空间申请是不需要初始值的,这也就是为什么许多初学者更多的接触malloc函数的原因。
如何初始化链表
1 |
|
附leetcode第二题:两数加和
1 | /** |
哈希表unordered_map、unordered_set
unordered_map
定义1 |
|
元素查找
1 |
|
unordered_set
定义1 |
|
元素查找与删除
在C++的 std::unordered_set 中,元素是无序存储的,并且没有类似于数组索引的概念来直接访问元素。你不能通过 hashSet[ 0 ]、hashSet[ 1 ] 这样的方式来访问元素,因为哈希集合的元素是通过其值进行唯一标识的,而不是通过索引。
在你的代码中,添加元素的顺序不会影响元素的存储和访问顺序。所以,无法根据索引来直接访问哈希集合中的元素。你只能使用 find 函数来检查元素是否存在,或者使用范围循环来遍历哈希集合中的元素,如我之前给出的示例代码所示。
std::unordered_set 中的 find 函数会返回一个迭代器,指向要查找的元素。如果元素不存在,它会返回迭代器等于 hashSet.end()。
1 |
|
1 |
|
如何在C++程序中定义计时器?
1 |
|
C++中函数的栈帧?
https://zhuanlan.zhihu.com/p/665191596
深拷贝与浅拷贝
浅拷贝例子
1 |
|
上面例子中,由于data是个指针,浅拷贝只会把指针本身粘贴过去,因此当改变Original中的内容时,shallow_copy也会被拷贝过去。
深拷贝的特点:
- 深拷贝是指在复制一个对象时,会复制该对象本身以及该对象所包含的所有数据,包括对象内部的所有数据成员,以及嵌套对象的数据。
- 深拷贝创建了一个完全独立于原始对象的新对象,它们不共享内部数据。
- 深拷贝通常涉及递归遍历对象的所有嵌套部分,确保每个部分都被复制,以便在新对象中保留相同的值。
浅拷贝的特点:
- 浅拷贝是指在复制一个对象时,只复制对象本身,而不复制对象内部的数据。
- 浅拷贝创建了一个新对象,该对象与原始对象共享内部数据,这意味着它们指向相同的数据成员或嵌套对象。
- 浅拷贝通常只涉及复制对象的成员指针或引用,而不复制指针或引用指向的实际数据。
typename
typename作用
typename关键字用于引入一个模板参数,这个关键字用于指出模板声明(或定义)中的非独立名称(dependent names)是类型名,而非变量名:
1 |
|
如何理解代码typedef typename std::vector::size_type size_type;
typename 在这里的意思表明 T 是一个类型。如果没有它的话,在某些情况下会出现模棱两可的情况,比如下面这种情况:
1 |
|
作者想定义一个指针iter,它指向的类型是包含在类作用域T中的iterator。可能存在这样一个包含iterator类型的结构:
1 |
|
那么 foo< ContainsAType>()>; 这样用的是时候确实可以知道 iter是一个ContainsAType::iterator类型的指针。但是T::iterator实际上可以是以下三种中的任何一种类型:
- 静态数据成员
- 静态成员函数
- 嵌套类型
所以如果是下面这样的情况:
1 |
|
那T::iterator iter;被编译器实例化为ContainsAnotherType::iterator iter;,变成了一个静态数据成员乘以 iter ,这样编译器会找不到另一个变量iter的定义 。所以为了避免这样的歧义,我们加上 typename,表示T::iterator一定要是个类型才行。
1 |
|
结论
我们回到一开始的例子,对于 vector::size_type,我们可以知道:
1 |
|
vector::size_type是vector的嵌套类型定义,其实际等价于size_t类型。
1 |
|
那么这个例子的真实目的是,typedef创建了存在类型的别名,而typename告诉编译器std::vector
类中的拷贝构造函数&&类中的运算符重载
链接:https://blog.csdn.net/qq_43519886/article/details/105170209
链接:https://blog.csdn.net/weixin_45031801/article/details/133993523
默认拷贝构造函数&&浅拷贝
如果没有显示定义拷贝构造函数,系统会默认生成该函数,但只是把复制对象的成员变量pass-by-value过来(默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝),如果成员变量中存在指针,那么寄了。
在C语言中,把实参传给形参是把实参的值拷贝给形参,如果靠pass-by-value那么会陷入到无限循环之中,因此拷贝构造函数必须传引用。此外为了考虑效率,内存等因素(值传递拷贝一个对象还会把其所有父类构造一遍),建议传引用,即pass-by-referance。
按需求实现的拷贝构造函数&&深拷贝
系统的默认拷贝构造函数不可能有深拷贝,都是自己动手实现的。也就是说想要拷贝构造的类中有new、malloc、fopen等行为,并且成员中有指针,那么我们就必须自己拷贝构造以达到深拷贝。
运算符重载
1 | //创建两个类对象,重载==运算符 |
//重载内容如下
1 |
|
调用的时候直接和内置类型进行运算符操作那样,编译器会自动处理成调用运算符重载的样子,不用operator==(d1,d2),而是d1==d2
const_cast、static_cast、dynamic_cast、reinterpret_cast
const_cast
- const_cast只针对指针、引用,当然,this指针也是其中之一。
- const_cast的大部分使用主要是将常量指针转换为常指针。常量指针指向的空间的内容不允许被修改,但是使用const_cast进行强制转换就可以修改。
- const_cast只能调节类型限定符,不能修改基本类型。在普通指针演示中给出示例。
- const_cast作用了之后还是const。
(1)普通指针1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
const int* p = new int(1);
//int* d = p;//错误原因:const int*类型不能赋值或者初始化int*类型的实体
*const_cast<int*>(p) = 50;
cout << *p << endl;//50,原来的常量被改变了
int* d = const_cast<int*>(p);
*d = 100;
cout << *p << endl;//100
//char* dd = const_cast<char*>(p)//错误原因:const_cast只能调节类型限定符,不能更改基础类型
return 0;
}
(2)引用
1 |
|
(3)this指针
1 |
|
static_cast
- static_cast的使用基本等价于隐式转换的一种类型转化运算符,可使用于需要明确隐式转换的地方。就相当于把隐式转换给明确写了出来而已。
- 什么叫低风险的转化,一般只要编译器能自己进行隐式转换的都是低风险转换,一般平等转换和提升转换都是低风险的转换。
- 整形和浮点型
- 字符与整形
- 转换运算符
- 空指针转换为任何目标类型的指针
- 不可以用于风险较高的转换
- 不同类型的指针之间互相转换
- 非指针类型和指针类型之间的相互转换
- 不同类型的引用之间的转换
- 关于继承关系的指针/引用转换:因为static_cast的转换时粗暴的,它仅根据类型转换语句中提供的信息(尖括号中的类型)来进行转换,这种转换方式对于上行转换,由于子类总是包含父类的所有数据成员和函数成员,因此从子类转换到父类的指针对象可以没有任何顾虑的访问其(指父类)的成员。而对于下行转换为什么不安全,是因为static_cast只是在编译时进行类型坚持,没有运行时的类型检查,具体原理在dynamic_cast中说明。
1 |
|
dynamic_cast
- dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。
- 当一个类中拥有至少一个虚函数的时候,编译器会为该类构建出一个虚函数表(virtual method table),虚函数表记录了虚函数的地址。如果该类派生了其他子类,且子类定义并实现了基类的虚函数,那么虚函数表会将该函数指向新的地址。虚表是C++多态实现的一个重要手段,也是dynamic_cast操作符转换能够进行的前提条件。当类没有虚函数表的时候(也即一个虚函数都没有定义),dynamic_cast无法使用RTTI,不能通过编译(个人猜想…有待验证)。当然,虚函数表的建立对效率是有一定影响的,构建虚函数表、由表查询函数 都需要时间和空间上的消耗。所以,除了必须声明virtual(对于一个多态基类而言),不要轻易使用virtual函数。对于虚函数的进一步了解,可以查看《Effective C++》
- 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,那么返回类型是一个空指针,结果是所需类型的空指针。
- 如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。
非必要不使用dynamic_cast,因为有额外的开销。
- 常用的转换方式
派生类指针或引用指向基类指针(上行转换)(因为都是安全的,所以可以使用dynamic_cast,但是更推荐用static_cast,dynamic_cast过于消耗资源)
基类指针或引用指向派生类指针(下行转换)(必须使用dynamic_cast,static_cast不会进行安全检查)
dynamic_cast只有当父类指针或引用真正指向子类对象时才能成功转换。说到下行转换,有一点需要了解的是在C++中,一般是可以用父类指针指向一个子类对象,如parent P1 = new Children(); 但这个指针只能访问父类定义的数据成员和函数,这是C++中的静态联翩,但一般不定义指向父类对象的子类类型指针,如Children P1 = new parent;这种定义方法不符合生活习惯,在程序设计上也很麻烦。这就解释了也说明了,在上行转换中,static_cast和dynamic_cast效果是一样的,而且都比较安全,因为向上转换的对象一般是指向子类对象的子类类型指针,转换成的父对象中的功能是子函数的一个子集,肯定都定义好了;而在下行转换中,想把父对象转化为子对象,由于子对象中功能很多(父对象功能是子对象的子集),转换后的子对象一定是功能缺失的,这时static_cast编译会过,但运行时会报错;而dynamic_cast是运行时类型检查,编译时发现功能不齐就不报错,而是运行时返回一个bad_cast或者空指针,就很灵活。下面通过代码进行说明
1 |
|
由于需要进行向下转换,因此需要定义一个父类类型的指针Base *P,但是由于子类继承与父类,父类指针可以指向父类对象,也可以指向子类对象,这就是重点所在。如果 P指向的确实是子类对象,则dynamic_cast和static_cast都可以转换成功,如下所示:
1 |
|
以上转换都能成功。但是,如果 P 指向的不是子类对象,而是父类对象,如下所示:
1 |
|
在以上转换中,static_cast转换在编译时不会报错,也可以返回一个子类对象指针(假想),但是这样是不安全的,在运行时可能会有问题,因为子类中包含父类中没有的数据和函数成员,这里需要理解转换的字面意思,转换是什么?转换就是把对象从一种类型转换到另一种类型,如果这时用 pd3 去访问子类中有但父类中没有的成员,就会出现访问越界的错误,导致程序崩溃。而dynamic_cast由于具有运行时类型检查功能,它能检查P的类型,由于上述转换是不合理的,所以它返回NULL。
例子2:
1 |
|
reinterpret_cast
这个和C语言的强制转换没什么区别,只不过C++用自己的写法替代了C语言的强制转换而已。
- 不同类型的指针之间的转换
- 指针和能容纳指针的整数类型之间的转换(比如将int类型强转成int*类型)
- 不同类型的引用之间的转换
编译期处理执行的是逐字节复制的操作。
这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。例如,程序员执意要把一个 int 指针、函数指针或其他类型的指针转换成 string 类型的指针也是可以的,至于以后用转换后的指针调用 string 类的成员函数引发错误,程序员也只能自行承担查找错误的烦琐工作:(C++ 标准不允许将函数指针转换成对象指针,但有些编译器,如 Visual Studio 2010,则支持这种转换)。