-
Notifications
You must be signed in to change notification settings - Fork 24
/
C++.cpp
2057 lines (1601 loc) · 63 KB
/
C++.cpp
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
C与C++的区别?
一、C++介绍
本贾尼·斯特劳斯特卢普,与1979年4月份贝尔实验室的本贾尼博士在分析UNIX系统分布内核流量分析时,希望有一种有效的更加模块化的工具。
1979年10月完成了预处理器Cpre,为C增加了类机制,也就是面向对象,1983年完成了C++的第一个版本,C with classes也就是C++。
C++与C的不同点:
1、C++基本兼容C的语法
2、支持面向对象的编程思想
3、支持运算符重载
4、支持泛型编程、模板
5、支持异常处理
6、类型检查严格
二、第一个C++程序
1、文件扩展名
.cpp .cc .C .cxx
2、编译器
g++ 大多数系统需要额外安装,Ubuntu系统下的安装命令:
sudo apt-get update
sudo apt-get install g++
gcc也可以继续使用,但需要增加参数 -xC++ -lstdc++
3、头文件
#include <iostream>
#include <stdio.h> 可以继续使用,但C++建议使用 #include <cstdio>
4、输入/输出
cin >> 输入数据
cout << 输出数据
cin/cout会自动识别类型
scanf/printf可以继续使用
注意:cout和cin是类对象,而scanf/printf是标准库函数。
5、增加了名字空间
std::cout
using namespace std;
三、名字空间
1、什么是名字空间
在C++中经常使用多个独立开发的库来完成项目,由于库的作者或开发人员没见过面,因此命名冲突在所难免。
2、为什么需要名字空间
在项目中函数名、全局变量、结构、联合、枚举、类,非常有可能名字冲突,而名字空间就对这些命名进行逻辑空间划分(不是物理单元划分),
为了解决命名冲突,C++之父为防止命名冲突给C++设计一个名字空间的机制。
通过使用namespace XXX把库中的变量、函数、类型、结构等包含在名字空间中,形成自己的作用域,避免名字冲突。
namespace xxx
{
}// 没有分号
注意:名字空间也是一种标识符,在同一作用域下不能重名。
3、同名的名字空间有自动合并(为了声明和定义可以分开写)
同名的名字空间中如果有重名的依然会命名冲突
4、名字空间的使用方法
::域限定符
空间名::标识符 // 使用麻烦,但是非常安全
using namespace 空间名; 把空间中定义的标识符导入到当前代码中
不建议这样使用,相当于把垃圾分类后,又导入同一个垃圾车,依然会冲突
5、无名名字空间
不属于任何名字空间中的标识符,隶属于无名名字空间。
无名名字空间中的成员使用 ::标识符 进行访问。
如何访问被屏蔽的全局变量。
6、名字空间的嵌套
名字空间内部可以再定义名字空间,这种名字空间嵌套
内层的名字空间与外层的名字空间的成员,可以重名,内层会屏蔽外层的同名标识符。
多层的名字空间在使用时逐层分解。
n1::n2::num;
namespace n1
{
int num = 1;
namespace n2
{
int num = 2;
namespace n3
{
}
}
}
7、可以给名字空间取别名
由于名字空间可以嵌套,这样就会导致在使用内层成员时过于麻烦,可以给名字空间取别名来解决这类问题。
namespace n123 = n1::n2::n3;
四、C++的结构
1、不再需要 typedef ,在定义结构变量时,可以省略struct关键字
2、成员可以是函数(成员函数),在成员函数中可以直接访问成员变量,不需要.或->,但是C的结构成员可以是函数指针。
3、有一些隐藏的成员函数(构造、析构、拷贝构造、赋值构造)。
4、可以继承,可以设置成员的访问权限(面向对象)。
五、C++的联合
1、不再需要 typedef ,在定义结构变量时,可以省略union关键字
2、成员可以是函数(成员函数),在成员函数中可以直接访问成员变量,不需要.或->,但是C的结构成员可以是函数指针。
3、有一些隐藏的成员函数(构造、析构、拷贝构造、赋值构造)。
六、C++的枚举
1、定义、使用方法与C语言基本一致。
2、类型检查比C语言更严格
七、C++的布尔类型
1、C++具有真的布尔类型,bool是C++中的关键字,在C语言中使用布尔类型需要导入头文件stdbool.h(在C11中bool应该是数据类型了)。
2、在C++中 true false 是关键字,而在C语言中不是。
3、在C++中 true false 是1字节,而C语言中是4字节。
八、C++的void*
1、C语言中void* 可以与任意类型指针 自动转换。
2、C++中void*不能给其他类型的指针直接赋值,必须强制类型转换,但其他类型的指针可以自动给void*赋值。
3、C++为什么这样修改void*?
为了更安全,所以C++类型检查更严格。
九、操作符别名
某些特殊语言的键没有~,&符合,国际标准化组织为一些操作符规定了别名,以便使用这些语言的键盘也能输入正确的C/C++代码。 C95和C++98以后的语言标准都支持ISO-646
and &&
or ||
not !
{ <%
} %>
# :%
十、函数重载
1、函数重载
在同一作用域下,函数名相同,参数列表不同的函数,构成重载关系。
2、重载实现的机制
C++代码在编译时会把函数的参数类型添加到参数名中,借助这个方式来实现函数重载,也就是C++的函数在编译期间经历换名的过程。
因此,C++代码不能调用C函数(C语言编译器编译出的函数)
3、extern "C" {}
告诉C++编译器按照C语言的方式声明函数,这样C++就可以调用C编译器编译出的函数了(C++目标文件可以与C目标文件合并生成可执行程序)。
如果C想调用C++编译出的函数,需要将C++函数的定义用extern "C"包括一下。
注意:如果两个函数名一样,一定会冲突。
4、重载和作用域
函数的重载关系发生在同一作用域下,不同作用域下的同名函数,构成隐藏关系。
5、重载解析
当调用函数时,编译器根据实参的类型和形参的匹配情况,选择一个确定的重载版本,这个过程叫重载解析。
实参的类型和形参的匹配情况有三种:
1、编译器找到与实参最佳的匹配函数,编译器将生成调用代码。
2、编译找不到匹配函数,编译器将给出错误信息。
3、编译器找到多个匹配函数,但没有一个最佳的,这种错误叫二义性。
在大多数情况下编译器都能立即找到一个最佳的调用版本,但如果没有,编译就会进行类型提升,这样备选函数中就可能具有多个可调用
的版本,这样就可能产生二义性错误。
6、确定存在函数的三个步骤
1)候选函数
函数调用的第一步就是确定所有可调用的函数的集合(函数名、作用域),该集合中的函数就是候选函数。
2)选择可行函数
从候选函数中选择一个或多个函数,选择的标准是参数个数相同,而且通过类型提升实参可被隐式转换为形参。
3)寻找最佳匹配
优先每个参数都完全匹配的方案,其次参数完全匹配的个数,再其次是浪费内存的字节数。
7、指针类型会对函数重载造成影响
C++函数的形参如果是指针类型,编译时函数名中会追加Px。
十一、默认形参
1、在C++中函数的形参可以设置默认值,调用函数,如果没有提供实参数,则使用默认形参。
2、如果形参只有一部分设置了默认形参,在某个提供了默认值的参数后面,所有的参数都必须提供默认值。
3、函数的默认形参是在编译阶段确定的,因此只能使用常量、常量表达式、全局变量数据作为默认值。
提问:如果函数的声明和定义需要分开,那么默认形参设置在声明、定义,还是声明定义都需要设置?
4、默认形参会对函数重载造成影响,设置默认形参时一定要慎重。
十二、内联函数
1、普通函数调用时是生成调用指令(跳转),然后当代码执行到调用位置时跳转到函数所在的代码段执行。
2、内联函数就把函数编译好的二进制指令直接复制到函数的调用位置。
3、内联函数的优点就是提高程序的运行速度(因为没有跳转,也不需要返回),但这样会导致可执行文件增大(冗余),也就是牺牲空间来换取时间。
4、内联分为显示内联和隐式内联
显示内联:在函数前 inline(C语言C99标准也支持)
隐式内联:结构、类中内部直接定义的成员函数,则该类型函数会被优化成内联函数。
5、宏函数在调用时会把函数体直接替换到调用位置,与内联函数一样也是使用空间来换取时间,所以宏函数与内联函数的区别(优缺点)?
1、宏函数不是真正的函数,只是代码替换,不会有参数压栈、出栈以及返回值,也不会检查参数类型,因此所有类型都能使用,但这样会
有安全隐患。
2、内联函数是真正的函数,被调用时会进行传参,会进行压栈、出栈,可以有返回值,并会严格检查参数类型,这样就不能通用,如果想被
多种类型调用需要重载。
6、内联适用的条件
由于内联会造成可执行文件变大,并增加内存开销,因此只有频繁调用的简单函数适合作为内联。
调用比较少的复杂函数,内联后并不显著提高性能,不足以抵消牺牲空间带来的损失,所以不适合内联。
带有递归特性和动态绑定特性的函数,无法实施内联,因此编译器会忽略声明部分的inline关键字。
十三、引用
引用就是取艺名(别名)。
1、引用的基本特性
引用就是取别名,声明一个标识符为引用,就表示该标识符是另一个对象的外号。
1、引用必须初始化,不存在空引用,但有悬空引用(变量死了,名还留着)。
2、可以引用无名对象(临时对象),但必须使用常引用。
3、引用不能更换目标
4、引用目标如果具有const属性,引用也需要具有const属性。
引用一旦完成了定义和初始化就和普通变量名一样,它就代表了目标,一经引用终身不能再引用其他目标。
2、引用型参数
引用当作函数的参数能达到指针同样的效果,但不具备指针的危险,还比指针方便。
引用可以非常简单的实现函数间共享变量的目的,而且是否使用引用由被调函数说了算。
引用当作函数的参数还能提高传递参数效率,指针至少还需要4字节内存,而引用只需要增加一条标识符与内存之间的绑定(映射)。
3、引用型返回值
不要返回局部变量的引用,会造成悬空引用。
如果返回值是一个临时值(右值),如果非要使用引用接收的话,必须使用常引用。
注意:C++中的引用时一种取别名的机制,而C语言中的指针是一种数据类型(代表内存编号的无符号整数)。
练习1:实现一个C++版本的swap函数。
指针和引用的相同点和不同点:
相同点:跨函数共享变量,优化传参效率,避免传参的时候调用拷贝构造
不同点:指针有自己的存储空间,借助指针可以使用堆内存,引用不行。引用取别名,指针是数据类型。指针可以为空,引用不可以为空。
指针可以不初始化,引用必须初始化。指针可以改变指向,引用不能引用其他对象(可以定义指针的指针,不能定义引用的引用。可以定义指针的
引用,不能定义引用的指针。可以定义指针的数组,但不能定义引用的数组。可以定义数组的引用)。
#include <iostream>
using namespace std;
void swap(int& a,int& b) //引用
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a=3,b=4;
swap(a,b);
cout<<a<<" "<<b<<endl;
}
十四、C++的内存管理
1、new/delete C++具备申请/释放堆内存功能的运算符
相当于C语言中的malloc和free。
new 类型:会自动计算类型所需要字节数,然后从堆中分配对应字节数的内存,并返回内存的首地址(具备类型)。
delete 指针:会自动释放堆内存。
注意:new/delete与malloc/free不能混用,因为new和delete会自动调用类、结构的构造函数、析构函数。
2、数组的分配与释放
new 类型[n]; n表示数组长度,如果类、结构会自动调用n次构造函数。
delete[] 指针;通过new[] 分配的内存,必须通过delete[]释放。
new[] 返回值前4个字节中存放着数组的长度。
3、重复释放
delete/delete[]不能重复释放同一块内存。
delete/delete[]释放野指针的后果不确定,但释放空指针是安全的。
#include <iostream>
using namespace std;
struct Student
{
Student(void)
{
cout<<"我是构造函数,创建对象时,我就会执行" << endl;
}
~Student(void)
{
cout<< "我是析构函数,释放对象时,我就会执行" << endl;
}
};
int main()
{
int* p = new int;
*p = 10;
cout<< *p <<endl;
Student stu;
Student* s = new Student;
delete(s);
cout << endl;
Student* a = new Student[3];
p = (int*)a;
cout << *(p-1) << endl;
delete[] a;
}
4、内存分配失败
当分配的内存过大,没有能满足需求的整块内存就会抛出异常,std::bad_alloc。
new/delete和C语言的malloc/free的相同点和不同点(区别)?
不同点:
身份 运算符 标准库函数
参数 类型(自动计算) 字节数(手动计算)
返回值 带类型的地址 void*地址
调用构造 自动调用 不能调用构造/析构函数
出错 抛出异常 返回NULL
相同点:
1、都能管理堆内存
2、不能重复释放
3、可以释放NULL
注意:在C++中尽量使用引用、new/delete
#include <iostream>
using namespace std;
int main()
{
int *p = NULL;
try{
p = new int[~0];
}
catch(std::bad_alloc& ex)
{
cout << "error" << endl;
}
}
十五、强制类型转换
面向过程编程:
关注是问题解决的过程步骤,算法
面向对象编程:
关注的是谁能解决问题(类),需要什么样的数据(成员变量),具备什么样的技能(成员函数)才能解决问题。
抽象:找出一个能够解决问题的“对象”(观察研究对象),找出解决所必须的数据(属性)、功能(成员函数)。
封装:把抽象的结果,归结为一个类(数据类型),然后实例化出类对象,设置对象的属性,调用对象的功能达到解决问题的目的。
继承:在解决问题前,先寻找之前的类能不能解决问题,或解决部分问题,如果可以则把旧的类继承后再次拓展,来缩短解决问题的时间,降低
解决问题的难度。
多态:对象的多种形态,外部看到一个对象发出指令,对象会根据自身情况做出独特的反应。
一、类和对象
1、通过分析对象的属性和行为设计出一个类。
2、类就是数据类型
简单类型:只能表示一个属性(变量),C/C++内建数据类型
数组类型:可以表示多个属性(变量),类型必须相同。
结构类型:可以表示多个属性(变量),但缺少行为(函数)。
类类型:即能表示属性,也能表示行为,一直复合数据类型。
3、对象就是类这种数据类型创建出的实例,相当于结构变量。
class Student
{
属性(成员变量);
行为(成员函数);
};
Student stu;
二、类的定义与实例化
1、类的一般形式
class 类名 : 继承方式 父类
{
public/private/protected: // 访问控制限制符
成员变量;
// 构造函数
类名(形参表)
{
}
// 析构函数
~类名(void)
{
}
};
2、类的访问控制限定符
public:公有成员,在任何位置都可以访问
private:私有成员,只能在类(自己)的成员函数中访问
protected:受保护成员,只能在类(自己)和子类中访问
注意:类中的成员变量、成员函数默认是 private,结构中的成员和成员函数默认是 public。
注意:C++中类和结构的区别只有成员函数和成员变量的默认访问权限不同。
3、构造函数
1)什么是构造函数:类的同名函数就是构造函数,没有返回值。
2)什么时候调用,谁调用,调用几次?
创建类对象时会被自动调用(每创建一个类对象,就会调用一次),对象整个生命周期中一定会被
调用一次,只能被调用一次。
3)负责干什么
成员变量的初始化,分配相关资源,设置对象的初始状态。
class 类名 : 继承方式 父类
{
// 构造函数
类名(形参表)
{
}
};
4、类型的创建过程
1.分配类型所需要空间,无论栈还是堆。
2.传递实参调用构造函数,完成如下任务:
1)根据继承表依次调用父类的构造函数
2)根据成员变量的顺序依次调用成员变量的构造函数。
3)执行构造函数体中的代码。
注意:执行构造函数的代码是整个构造函数的最后一步。
要保证构造函数代码所需要的一切资源和先决条件在该代码执行前已经准备充分,并得到正确的初始化。
5、对象的创建方法
1.在栈上创建:类名 对象;// 不需要括号
类名 对象(实参);
2.在堆上创建:类名* 对象指针 = new 类名;
类名* 对象指针 = new 类名(实参);
3.创建多个对象:
类名 对象 = {类名(实参),类名(实参),类名(实参)};
类名* 对象指针 = new 类名[n]{类名(实参),类名(实参)};
注意:通过malloc创建的类对象不能调用构造函数。
注意:通过new[]创建的对象,一定要通过delete[]释放。
6、类的声明、实现、调用
1.在头文件中声明
class 类名 : 继承方式 父类
{
成员变量;
public: // 访问控制限制符
// 构造函数
类名(形参表);
// 析构函数
~类名(void);
// 其他成员函数
返回值 函数名(参数列表);
};
2.源文件实现类的相关函数
返回值 类名::函数名(参数列表)
{
}
3.调用时只需要导入头文件,然后与类函数所在的源文件一起编译即可。
注意:如果一个类内容不多,可以考虑在头文件中完全实现。
也可以只在头文件中实现一些简单的成员函数。
注意:类中自动生成的函数,在源文件中实现时,也需要在头文件中声明。
class和struct的区别?
class的默认继承和访问权限是private,struct的默认继承和访问权限是public。class能做模板的参数,struct不行。
三、构造函数与初始化列表
1、构造函数可以被重载(同一个名字的函数有多个不同版本)
2、缺省构造是编译器自动生成的一个什么都不做的构造函数(唯一的作用就是避免编译错误)。
注意:当类实现一个有参构造时,缺省构造就不会再自动生成,如果有需要必须显示地写出来。
3、无参构造未必无参,当给有参构造的所有参数设置默认形参,调用这种构造函数就不需要传参。
注意:所谓的“编译器生成的某某函数”其实不是真正语法意义上的函数,而是功能意义上的函数,编译器作为可执行指令的生成者,它会直接生成
具有某项功能的二进制指令,不需要借助高级语言语义上的函数完成此任务。
注意:如果一个类是其他类的成员变量,那么一定要保证它有一个无参构造,当B的构造函数执行时会执行成员变量的无参构造,而此时类B是无
法给类A成员变量提供参数的。
4、单参构造与类型转换
如果构造函数的参数只有一个,那么Test t = n语句就不会出错,它会自动调用单参构造来达到类型转换的效果。
如果想禁止这种类型转换需要在单参构造前加 explicit
5、初始化列表
为类成员进行初始化用的。
构造函数(参数):成员1(参数1),成员2(参数2)...
const int num;
Test(int n):num(n)
{
}
通过初始化列表可以给类成员变量传递参数,以此调用类成员的有参构造。
初始化列表也可以给 const 成员、引用成员进行初始化。
#include <iostream>
using namespace std;
class A
{
public:
int num;
A(int _num)
{
num = _num;
cout<<"我A的有参构造"<<endl;
}
};
class Test
{
public:
string str;
const int num;
int& xiu;
A a;
Test(int num,const char* str,int& x):num(num),str(str),a(num),xiu(x)
{
cout<<"---"<<endl;
}
};
int main()
{
int x = 100;
Test t(10,"aa",x);
//Test t = {10};
//t.num = 100;
cout<< t.num << endl;
cout<<t.xiu<<endl;
x = 1000;
cout<<t.xiu<<endl;
}
成员的初始化顺序与初始化列表没有关系,而是在类中的定义顺序有关。
#include <iostream>
using namespace std;
class A
{
public:
A(int n)
{
cout<<"A"<<endl;
}
};
class B
{
public:
B(int n)
{
cout<<"B"<<endl;
}
};
class C
{
public:
C(int n)
{
cout<<"C"<<endl;
}
};
class Test
{
public:
A a;
C c;
B b;
//A a;
Test(int c1,int a1,int b1):c(c1),a(a1),b(b1)
{
}
};
int main()
{
// A a1;
// B b1;
// C c1;
Test t(1,2,3);
}
注意:初始化列表运行类成员变量还没有定义成功。
作业1:封装一个List类
附加题:以C++编程方式实现2048游戏。
一、this指针
类的成员变量单独存储在每个类对象中,成员函数存储在代码段中,所有的类对象共享一份成员函数。
成员函数是如何区别调用它的是哪个类对象的?
答:借助了this指针,类的每个成员函数都有一个隐藏的参数this指针,它指向类对象。
类的构造函数中也同样有this指针,指向的就是正在构造的这个对象。
在类中(成员、构造、析构函数)对成员变量、成员函数的访问都是借助了this指针。
this指针是隐藏的,但也可以显示使用:
1、参数与成员一样时,使用this可以区别出成员与参数名。
2、在成员函数中如果想返回当前对象的指针、引用等,可以使用this指针实现。
3、将this指针作为函数的参数,从一个对象传递给另一个其它类对象,可以实现对象间的交互。
#include <iostream>
#include <cstring>
using namespace std;
class User
{
char name[20];
char pass[7];
public:
User(const char* name,const char* pass)
{
strcpy(this->name,name);
strcpy(this->pass,pass);
//show();
}
User& func(void)
{
return *this;
}
void show(void)//隐藏this指针
{
cout<< name << " "<< pass <<endl;
}
User* this = const this;
};
int main()
{
User u1("aaa","123");
User u2("bbb","321");
User& u3 = u1.func();
u1.show();
u2.show();
u3.show();
}
二、常函数
在函数的参数列表与函数体之间有const修饰的函数,这个const其实就是在修饰this指针。
不能在常函数内修改成员变量的值,普通成员函数可以调用常函数,而常函数只能调用常函数。
如果在常函数中真的需要修改某个成员变量的数据,那么需要这个成员被 mutable修饰。
mutable char name[20];
void show(void) const//隐藏this指针
{
strcpy(name,"------");
cout<< name << " "<< pass <<endl;
}
普通函数不能声明为常函数(因为没有this指针)。
三、析构函数
1、特殊的成员函数
~类名(void)
{
}
没有参数、没有返回值、不能重载
2、谁来调用
析构函数会在销毁对象时自动调用,在对象的整个生命周期内最多被调用一次。
3、析构函数负责什么
负责释放在构造函数期间获取的所有资源,它的执行过程:
1.先执行析构函数本身代码
2.调用成员类的析构函数
3.调用父类的析构函数
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class A
{
public:
A(void)
{
cout << "A 's 构造" <<endl;
}
~A(void)
{
cout<<"A 's 析构"<<endl;
}
};
class B
{
public:
B(void)
{
cout << "B 's 构造" <<endl;
}
~B(void)
{
cout<<"B 's 析构"<<endl;
}
};
class User : public A
{
char* name;
char* pass;
B b;
//char name[20];
//char pass[10];
public:
User(const char* name,const char* pass)
{
this->name = new char[strlen(name)+1];
strcpy(this->name,name);
this->pass = new char[strlen(pass)+1];
strcpy(this->pass,pass);
cout<< "构造"<<endl;
cout<<"-----"<<endl;
}
/* User(void)
{
cout<<"构造"<<endl;
}
*/
~User(void)
{
delete name;
delete pass;
cout<<"析构"<<endl;
}
};
int main()
{
User* u1 = new User("asd","ads");
//exit(0);
delete u1;
//User u2;
}
4.缺省的析构函数
如果一个类没有实现析构函数,编译器会自动生成一个具有析构函数功能的二进制指令,它负责释放编译器能够看得到的资源(成员变量、
类成员、弗雷成员),这就是缺省析构。
如果类中没有动态资源,也不需要做善后工作,缺省析构就完全共用了,不需要再实现新析构函数。
注意:缺省析构无法释放动态资源(堆内存)【堆内存是动态资源,动态资源不一定是堆内存】
作业:类对象的创建过程与释放过程。
创建:分配内存(对象)-> 父类构造-> 成员构造-> 自己构造
父类构造:按照继承表从左到右依次构造。
成员构造:按照声明顺序从上至下依次构造。
释放:自己析构-> 成员析构-> 父类析构-> 释放内存(对象)
成员析构:按照声明顺序从下到上依次构造。
父类析构:按照继承表从右到左依次构造。
四、拷贝构造
拷贝构造又称为复制构造,是一种特殊的构造函数,它是使用一个现有的旧对象构造一个新的对象时调用的函数,只有一个引用型的参数(对象本身)。
类名(类& )
{
}
拷贝构造的参数应该加 const 保护,但编译器并没有强行限制。
编译器会自己生成一个拷贝构造函数,它负责把旧对象中的所有数据拷贝给新创建的对象。
深拷贝与浅拷贝的区别:
如果类成员有指针,浅拷贝只拷贝指针变量的值,而深拷贝指针变量所指向的目标。
什么情况下需要实现拷贝构造:
当类成员中有指针成员,此时默认的拷贝构造(浅拷贝)就无法完成任务,需要自己动手实现拷贝构造(深拷贝)。
什么情况下会调用拷贝构造:
1、使用旧对象给新对象赋值时
User user1 = user;
2、使用对象当作函数的参数,当调用函数时,就会一起调用拷贝构造。
#include <iostream>
#include <cstring>
using namespace std;
class User
{
char* name;
char pass[7];
int id;
public:
User(const char* name,const char* pass)
{
this->name = new char[strlen(name)+1];
strcpy(this->name,name);
strcpy(this->pass,pass);
}
void show(void)
{
cout<<name <<" " <<pass <<endl;
}
~User(void)
{
cout<<"析构"<<&name <<endl;
delete[] name;
}
User(User& that)
{
name = new char[strlen(that.name)];
strcpy(name,that.name);
strcpy(pass,that.pass);
cout << "我是拷贝构造" << endl;
}
};
void func(User& user)
{
user.show();
}
int main()
{
User u1("a","aa");
u1.show();
// 调用拷贝构造
User u2 = u1;
u2.show();
func(u1);
}
五、赋值构造(赋值运算符)
当一类对象给另一个类对象赋值时,就会调用赋值构造
void opeator = (类&)
{
}
什么时会调用:对象 = 对象;
编译器会生成一个缺省的赋值构造,它负责把一个对象的内存拷贝给另一个对象。
什么情况需要实现赋值构造:
当需要深拷贝时,需要自己动手实现赋值构造,也就是拷贝构造与赋值构造需要同时实现。
编译器会自动生成四个成员函数:构造、析构、赋值构造、拷贝构造。
#include <iostream>
#include <cstring>
using namespace std;
class User
{
char* name;
char pass[7];
public:
User(const char* name,const char* pass)
{
this->name = new char[strlen(name)+1];
strcpy(this->name,name);
strcpy(this->pass,pass);
}
void show(void)
{
cout<<name <<" " <<pass <<endl;
}
~User(void)
{
cout<<"析构"<<&name <<endl;
delete[] name;
}
User(User& that)
{
name = new char[strlen(that.name)];
strcpy(name,that.name);
strcpy(pass,that.pass);
cout << "我是拷贝构造" << endl;
}
User& operator = (const User& that)
{
cout<< this <<" "<< &that << endl;
if(this != &that)
{
cout<<"我是赋值构造"<<endl;
// 释放旧空间
delete[] name;
// 申请新空间
name = new char[strlen(that.name)+1];
// 拷贝内容
strcpy(name,that.name);
strcpy(pass,that.pass);
/*
User temp(that);
swap(name,temp.name);
*/
}
return *this;
}
};
int main()
{
User u1("a","aa");
User u2("bbbb","bb");
User u3("ccc","cc");
//赋值构造
u1 = u1;
//u2 = u1 = u3;
u1.show();
u2.show();
u3.show();
}
六、关于拷贝构造、赋值构造的建议
1、缺省的拷贝构造、赋值构造函数不光会拷贝本类的数据,也会调用成员类对象和父类的拷贝构造和赋值构造,而不是单纯的按字节复制,
因此尽量少用指针成员。
2、在函数参数中,尽量使用类指针或引用来当参数(不要直接使用类对象),减少调用拷贝构造和赋值构造的机会,也可以降低数据传递的开销。
3、如果由于特殊原因无法实现完整的拷贝构造、赋值构造,建议将它们私有化,防止误用。
4、一旦为一个类实现了拷贝构造,那么也一定要实现赋值构造。(<=>)
七、静态成员
类成员一旦被 static 修饰就会变成静态成员,而是单独一份存储在bss或data内存段中,所有的类对象共享(静态成员属于类,而不属于某个对象)。
静态成员在类内声明,但必须在类外定义、初始化。与成员函数一样需要加“类名::”限定符表示它属于哪个类,但不需要再额外增加 static
成员函数也可以被static修饰,这种函数叫静态成员函数,这种成员没有this指针,因此在静态函数中不能直接访问类的成员,但可以直接访问
静态成员,但可以直接访问静态成员变量、静态成员函数。
静态成员变量、函数依然受访问控制限定符的影响。
因为在代码编译完成后,静态成员已经定义完成(有了存储空间),一次可以不用活类对象而直接调用,类名::静态成员名
静态成员变量可以被当做全局变量来使用(访问限定符必须是public),静态成员函数可以当作类的接口,实现对类的管理。
八、单例模式
什么是单例模式,只能创建出一个类对象(只有一实际的实例)的叫单例模式。
单例模式的应用场景:
Windows系统的任务管理器
Linux/Unix系统的日志系统
网站的访问计数器
服务端程序的连接池、线程池、数据池
获取单一对象的方法:
1、定义全局(C语言),但不受控制,防君子不能防小人。
2、专门写一个类,把类的构造函数设置私有,借助静态成员函数提供一个接口,以此来获取唯一的实例。
C++如何实现单例:
1、禁止类的外部创建类对象:构造函数设置私有
2、类自己维护一个唯一的实例:使用静态指针指向
3、提供一个获取实例的方法:静态成员函数获取静态指针
饿汉模式:
将单例类的唯一实例对象定义为成员变量,当程序开始运行时,实例对象就已经创建完成
优点:加载进程时,静态创建单例对象,线程安全。
缺点:无论使用与否,总要创建,浪费内存。
懒汉模式:
用静态成员指针来指向单例类的唯一实例对象,只有真正调用获取实例的静态接口时,实例对象才被创建。
优点:什么时候用什么时候创建,节约内存。
缺点:在第一次调用获取实例对象的静态接口时,才真正创建,如果在多线程操作情况下有可能被创建出多个实例对象(虽然可能性很低),
存在线程不安全问题。
总结:C语言与C++有哪些不同点
内存管理 malloc/free new/delete
static
const
void*
字符串:string系列函数 string类
一、操作符函数重载
什么是操作符函数:在C++中针对类类型的对象的运算符,由于它们肯定不支持真正的运算操作,因此编译器会将它们翻译成函数,这种就叫做
操作符函数(运算符函数)。
编译器把运算翻译成运算符函数,可以针对自定义的类类型设计它独有的运算功能。
其实各种运算符已经具备一些功能,再次实现它的就是叫作运算符重载。
双目运算符:
a+b