-
Notifications
You must be signed in to change notification settings - Fork 1
/
C++notes.txt
500 lines (351 loc) · 31.7 KB
/
C++notes.txt
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
9.1
注意scanf()的返回值是直到第一个格式出错的输入变量前的所有正确输入个数,若第一个输入格式便出错则直接返回0.
9.2
for(statement1;statement2;statement3)其中语句3一定是在执行完循环中内容之后再执行,语句3处写i++和++i效果一样,但++i比i++性能好
设置断点,调试加单步执行可监控各个变量的值
可用VS自带的分析功能生成代码运行报告,可查看运行时间,占用内存等
9.3
C语言定义数组长度只能用常量来定义,不能int n=10; float array[n];因为此时n为变量,可用const使n变为常量(注意,在一些C标准下const int n=0;为常变量,即值固定的变量,其属性仍为变量,但在C++中带const就直接被判定为常量)
对于数组array[10]而言, &array[i] 和 array+i 含义一样,都表示第n个元素的地址。但注意array不能进行自增操作,因为它是常指针,指向的地址是不能变的,array++是错误的,但array+i可以是因为array+i没有改变array指向的值。
9.4
{0}可将数组内容全置为0
一维数组的每个元素都是一维数组,组成的便是二维数组。
二维数组按元素初始化时是由先列递增而后再行递增的顺序存储每个数字的(类似于人正常读文章的顺序),与二维数组在地址空间中的存储方式相同。
9.5
注意gets()接收换行符之前的内容作为输入数组,换行符不会被接收,因此可能产生换行符余留问题。
puts()输出自动换行
9.6
&为取地址操作。*为取内容操作,&(*ptr)还是指针ptr,*(&a)还是变量a
指针变量可以和其他变量一起定义
指针指向的地址大小即其类型数据所占的地址大小
指针变量一定要初始化,如果是空的就赋值为NULL,否则会出现野指针问题。
神出鬼没野指针,地老天荒死循环
相同类型指针可以相互赋值。
对于二维数组,先访问行再访问列,行指针的定义方式为
int (*p)[4];//这定义了每行有4个元素(即数组有4列)的行指针
此时p[i]表示的是第i行(默认为第一个元素)的地址,p[i]+j 表示的是i行j列的地址,与 p[i][j] 等价
*(p[i]+j) 表示i行j列的元素,也可以写成 *(*(p+i)+j) 或者 (*(p+i))[j]
nt *p=*a//这定义了二维数组列指针,就相当于把二维数组按内存中存储顺序一字铺开,此时 p[i*n+j] 其中n为数组的列数,p[i*n+j] 表示的是i行j列的地址
在声明时可以直接赋值 int i=0; int *p=&i; 因为在声明时的*是说明符号,说明变量类型为指针
但在声明之后
int i=0, *p;
p=&i;// 此时不带*号表示的是将&i即i的地址赋值给p指针,在不带*单独有p时表示在内存中属于该指针的的地址。可将指针p比作盒子,指针指向的地址&i比作盒子中的纸条,纸条上记着另一个盒子i的位置。这个操作就相当于将写着位置的纸条放入盒子中
*p=100;//此操作含义为将100赋值给i,此处的*是运算符,即访问这个指针值向的地址,带*号的*p表示这个指针指向的地址。即纸条的内容。这个操作相当于通过这个盒子中的纸条*p访问盒子i并将100存入其中。
9.7
strcat string catenate
typedef定义新的变量类型,可用于结构体类型的命名
clock_t clock(void)可返回程序执行时间
int *p[4];//此为指针数组
字符指针数组存放的是指向字符串的指针,其指向的字符串长度不必预先定义,可以是不规则的长度
定义为 char *str[100];//这是100个字符串指针,存的是100个字符串各自的首字符地址,分配的空间是 4x100+每个字符串实际的空间
但字符二维数组定义时便确定了空间大小,比如char str[100][50];//定义了100个字符串的数组,其中字符串的最大长度为50,分配的空间就是50x100=500,相比之下字符指针数组更加灵活
//字符指针数组实战练习(待定)??
二重指针:在二重指针中存放的是指向另一个变量的指针的地址,二重指针为指向指针的指针
定义为**p
注意,指针的地址是不能赋值给普通指针的,只有二重指针可以指向指针
注意,字符数组和字符串是有区别的
定义
char carray[]={'f','u','c','k'};
char cstring[]="fuck";等价于char cstring[5]="fuck";//cstring的长度至少得是5,因为有结束符
二者是不一样的,carray是数组,没有结束符号"\0",但cstring有结束符,因此carray不能用%s来输出(除非在数组中人为添加结束符),但cstring可以用%s输出
char *p="hello";//这条语句在内存中以常量的方式存储了一个字符串hello,并把这个字符串的首地址赋值给了指针p,而且不能通过对p重新赋值来修改字符串的值,因为hello是常量不能被修改,但可以通过改变p的指向使其指向不同的内容
9.8
在遇到问题时要善用单步调试查看变量来解决问题(重要)
计算字符串长度时不包括结束符 '\0'
-> 为指向成员运算符,在通过结构指针访问结构体时用到这个符号
注意!!
float c = 5 / 9;
printf("%f", c);
输出的是0.00000 因为5/9中的5和9默认都是按照int存储的,因此int/int会将int的结果赋给c
因此应该用
float c = 5.0 / 9.0;
printf("%f", c);
变为5.0和9.0之后这两个数类型为float,可得出正确结果
>>可连续输入多个数据,输入方式为每行只输入一个数据,用回车结束,或者每行输入多个数据,用空格分割,回车结束输入
C++通过new 和delete管理动态内存
new执行失败返回NULL
使用方法为
int *p;
p=new int;//这样是为p分配一个int类型的空间
delete p;
p=new int(10);//这样是为p分配一个int类型的空间,并向其中存入10
delete p;
p=new int[10];//这样是为p分配10个int类型的内存,即一个10位的int数组,p指向首地址
delete []p;//其中的[]代表p指向的是数组空间
9.9
函数重载:同名的,参数个数或者类型不同的函数,系统会根据输入自动判断使用同名函数中的哪一个,出现混淆时会报错。
一般只把相同功能的函数进行重载,不同功能的函数不建议同名
注意:不能以形参名字或者返回值类型不同来区分函数,只能以形参个数或类型来区分
在声明函数的时候,默认形参值必须按照从右向左的方式声明,有默认形参值的参数一律放到右边,有默认值的形参右边不能再有无默认值的形参
在不同的作用域内声明同一函数时,允许声明不同的默认形参值,且声明的默认形参值仅在声明的作用域内有效
9.10
宏定义替换中,内容带有++时容易出错
如 #define abs(a) ( (a) < 0 ? -(a) : (a) )
int m=-2; ret=abs(++m);
正确输出应为1
但最终输出ret的结果是0
因为用++m替换得到的表达式为
ret=( (++m) < 0 ? -(++m) : (++m) );
其中++m执行了两次
宏定义替换不能带分号
宏定义的好处是不需要调用函数,比调用函数占用的系统资源更少,且宏定义的参数适用于大多数类型的数据
缺点是没有类型检查,副作用可能难以预料
内联函数:
在定义前加上 inline 即表示该函数为内联函数
程序在执行到内联函数的时候先检验参数类型,而后直接用函数体对这段代码进行替换
一般只把需要频繁调用的,长度较短只有几条语句的定义为内联函数
内联函数中不允许含有循环语句和switch语句,否则自动按照普通函数来处理
内联函数是C++中的一种特殊函数,它可以像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。一般来说inline用于定义类的成员函数。
(1)类内声明并定义成员函数时,不用在函数头部加inline
(2)类内声明,类外定义成员函数时要在类外定义时加上inline
C语言不认可const为常量,认为其是值不能改变的变量,因此C语言中const定义的内容不能作为数组长度等
但C++中可以将const作为常量,声明时必须赋初值
独立引用
int m;
int &pm=m;
此时pm与m指向的是同一个地址,pm是m的引用,相当于pm是m的别名
const float &pm=1.0;//定义r2为实数常量,值为1.0
定义常引用
int x=1;
const int &rx=x;
此时rx只能被读出但不能被修改
9.11
定义指针调用函数
int func ( int *p){}
调用该函数
func(&a);
对于形参为指针的,在调用时应当向其中传入地址
在调用函数时,仅仅把实参的值拷贝一份副本传给形参而后执行函数,不会改变实参的值
要改变实参值,在C语言中可用指针调用,需要特地为指针分配空间
在C++中还可用引用调用,将函数的形参以引用形式声明,在调用时形参作为实参的一个引用或者说别名,对形参的操作就是对实参本身的操作
引用调用不需要额外为形参的指针开辟地址,因此更节省空间。不需要通过指针间接访问,而是直接访问实参的内容,因此更节省时间。
引用调用举例:
int func(int &m){}//参数值为引用
int &func(int m){}//返回值为引用
当返回值为引用时可以出现在等号左侧,相当于把等号右侧内容赋给返回值的地址
面向过程:首先考虑步骤
面向对象:首先考虑数据,要获取那些数据,要对数据进行那些处理
将多个数据组成的对象作为基本数据单元
指定基本类型:1 定义内存数量
2 定义如何解释内存中的位
3 定义可对对象(数据)进行怎样的操作或称方法
9.12
函数的返回值不能是局部变量的地址
9.13
执行函数时占用栈中的空间,一旦函数运行完毕返回,则栈中空间释放,这就是函数中对形参进行操作不影响实参的原因
输出参数:将函数形参定义为传递地址则可实现用参数输出,只需在执行函数后访问传入的地址即可实现输出功能。
9.14
大多数递归函数都能用非递归函数(如循环)代替
递归函数的优势在于简化程序,但缺点是系统开销大,递归会占用大量的栈空间
非递归(如循环等)执行速度比递归快,也更节省空间
const表明该变量类型为常数
static表明该变量的存储类型为静态,即该变量的存储位置固定?、生存期为整个程序,在函数体内声明的静态局部变量性质与全局变量相同,在某处声明一次后,只要程序在运行,这个变量就会一直生存下去,反复执行该函数也不会对该变量进行重新声明
extern表示外部变量,非静态的extern允许其他文件引用,静态的static extern只限本文件引用
全局变量前带static可限制其引用范围为这个文件内,不带static其范围为整个工程(整个源程序)
做较大项目时前期应将大问题分解成小问题(模块化的自顶向下的程序方法),考虑如何针对每个小问题设计各个模块,并尽量增加模块的可复用性,简化程序设计
在字符串中取出char型的某数字x,实现从字符到数值的转化只需要 'x'-'0' 因为数字0-9在ascii上是连号的,'x'-'0'的值正好是x
一个函数判断多个结果可以通过返回不同的数字,比如第一种结果返回1,第二种返回2,然后在主函数中善用switch case对不同结果执行不同操作
一般用return -1;来表示出错
调试程序F11进入函数
9.15
先对问题进行抽象,对具体问题进行概括,提炼出这一类对象的公共性质并加以描述
数据抽象:描述某类对象共有的属性或者状态
行为抽象:描述某类对象共有的行为特征或者具有的功能
抽象 -> 封装 -> 类
先声明再实现,先画出类的框架与各个成员(包括数据成员和函数成员)再逐步实现
一般不在声明类的语句块里直接实现类的各个函数,而是在外部通过 :: 符号编写类内部的成员函数
类和实例的关系犹如模子与铸件
类内成员默认为private,需要定义public时需特殊声明
公有成员是类与外部的接口,可被任何单位访问
private私有,只能被类内部的成员访问,在类外部被看到或者访问
主程序使用类时:对象.方法
指向类对象的指针访问类中成员时用 ->
所有类对象都有this指针,它指向了当前正在被成员函数操作的这个对象空间首地址
例如当类函数的形参与该类中的数据成员重名时,在名称前加上this - > 表示的是类中的数据成员
this指针不能被修改,总是指向当前的调用对象
对象引用往往比对象指针更方便,更简洁
静态成员函数与静态成员数据一样,属于类而不属于对象,因此也没有this指针
友元函数与普通函数相同, 无this指针,不归属于某个类,不是类的成员,在类外可以直接调用
9.16
类的构造函数是类的一种特殊的成员函数,本质上也是类的一种成员函数,它会在每次创建类的新对象时执行,完成对象的初始化操作。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void(这里注意,返回值直接不要写,写void是错的,荣誉忽略)。构造函数可用于为某些成员变量设置初始值。
默认的构造函数没有任何参数,不做任何工作,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。
构造函数可以通过赋值语句或者表达式表的方式来对对象进行初始化
举例:声明时钟类,类包含三个数据成员时分秒
class Clock{
private:
int hour,min,sec;
public:
Clock(int h,int m,int s;);//此处声明了构造函数
};
赋值语句方式实现构造函数
Clock(int h,int m,int s;)
{
hour=h;
min=m;
sec=s;
}
表达式表方式实现构造函数
Clock (int h,int m,int s): hour(h), min(m), sec(s) { }
若构造函数只有一个参数,则可通过直接等号赋值的方式传递参数如func=1,否则需要与形参对应如
Clock Clock(12,15,10);
在创建类对象时,若构造函数无默认参数值,则必须给每个参数赋初值,不能为空
但构造函数可以重载,以满足不同的初始化需要
注意:若构造函数给参数赋了默认值,则不能再声明一个没有参数的重载,否则那个没有参数的重载是无效的
例如先声明了
Clock(int h=0,int m=0, int s=0);
则之后再声明一个重载
Clock();
是无效的,因为系统不能区分你不带参数时是调用的这两函数中的哪一个
析构函数:C++通过析构函数来处理对象被销毁时的清理工作,析构函数没有返回值类型,没有参数,其函数名是在类名前加~
与构造函数相同,C++系统默认为每个类带有默认的析构函数,默认的析构函数不做任何动作
析构函数也是可以被修改的,例如将其功能设置为输出一句话"Object has been desructed"
将构造函数设置为输出一句"Object is being created"
二者结合即可在控制台中看到该类的创建与销毁,在实际应用中可用于监测变量的生存周期。
默认析构函数不做动作,因此有时在函数中创建类分配的空间在函数运行结束后仍然不会销毁,可以使用自定义的析构函数来销毁这些空间
若在类的函数中存在指针或动态内存分配则要额外关注,其分配的动态内存很可能出现没有删除的问题,可以通过自定义析构函数来解决,但也要注意对同一个空间删除两次也会导致错误
拷贝构造函数:在构造函数中,将一个与自己同类的对象的引用作为参数进行构造函数的初始化
在Clock类中定义
Clock(int h,int m,int s):hour(h),min(m),sec(s) { }
Clock(Clock &obj) //拷贝构造函数
{
hour= obj.hour;
min=obj.min;
sec=obj.sec;
}
在主函数中使用时可以
int main(){
Clock c1(12,10,15);
Clock c2(c1);//此处便是调用拷贝构造函数,用c1来初始化c2
Clock c3=c1;//拷贝构造函数也可以写成这种形式
}
C++为每个类提供默认的拷贝构造函数,其功能与以上类似,不过缺省拷贝构造函数是位拷贝,即将c1的每一位拷贝到c2,又称精确拷贝函数
默认拷贝函数的问题在于,在涉及到动态内存分配时,默认的拷贝构造函数会将两个对象中的指针指向同一内存空间,因此在释放空间时会对一个空间释放两次,出现错误
!!!注意:对象作为函数参数或返回值时也会自动调用拷贝构造函数,通过拷贝参数来向函数传递参数或者返回值,因此若拷贝构造参数中有其他动作则会在每次调用对象作为参数或者返回值时都执行,此处在做题时容易被忽略
将类参数传值到函数中时,需要通过拷贝构造函数,将值单独拷贝一份传入函数中,花费时间、占用系统资源都较多,因此不建议使用
指针也要分配一个很小的指针空间,额外占用一些内存
更推荐将对象引用作为参数,引用不是定义一个新的变量或对象,因此内存不会为引用开辟新的空间存储这个引用,更节省时间和空间
在函数体外定义的内置数组,即全局变量数组,默认隐式初始化为0 // 检验
在函数体内定义的内置数组默认无初始化,是混乱的值
类类型的数组自动调用该类的默认构造函数进行初始化,若无默认构造函数,则必须为该数组的元素提供显式初始化,否则出错
对象数组的初始化过程实际上是调用构造函数,对每一个元素进行初始化的过程
对象指针也可直接进行P++这样的加减操作,指向数组中下一个类元素的首地址
静态数据成员(static) 在类内部进行声明,必须在类外部进行定义和初始化
变量的声明和定义:从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。因此定义一定包括了声明。
静态数据成员的特殊性:若在某类中声明了一个静态数据成员,并且在类外进行了定义和初始化,那么这个成员的地址就固定了,固定为了初始化时的地址,若在主函数中创建该类的多个对象,尽管对象名不同,但这多个对象共用那一个静态成员,在这多个对象中的静态成员均为一开始初始化的那个成员,无论通过哪个对象来访问这个静态成员,访问的都是一开始初始化时的那个地址。
静态数据成员并不归属于某一个具体的对象,而是属于这个类本身,因此即使这个类没有对象,也能访问这个类的静态数据成员
静态成员函数与静态成员数据一样,属于类而不属于对象,因此也没有this指针
在类外只能访问public型的静态数据,通过作用域符号 :: 来访问,
构造函数默认为public,若将构造函数声明为private类型,则无法直接被外部实例化,但若在函数内部提供static类的方法来访问本身的构造函数,并在类的成员函数中new出一个实例,则可保证该类在整个程序运行过程中只存在这一个实例。
%0|%0 查含义
查new 和 delete
markdown
9.17
const可定义常对象或者常成员
常对象不能被赋值或者修改,常对象不能调用非常成员函数,只能调用常成员函数
常数据成员只能通过初始化列表来获得初值
常成员函数不能修改对象数据成员的值,不能调用该类中没有用const修饰的成员函数,只能调用该类中的常数据成员
在实现常成员函数时也要加const标志
一个对象的private成员只能被自己的成员访问到,因此类外的对象不能直接访问private类的成员,只能通过公有成员间接访问
一个类的友元可以访问到这个类的私有成员,
在类A的声明中将另一函数B声明为友元函数可以使得B访问到这一类的所有成员
友元函数的声明可以放在类内的任何位置
友元函数与普通函数相同, 无this指针,不归属于某个类,不是类的成员,在类外可以直接调用
在A类中声明
friend class B;(注意应当先有B类的类声明,才能将B类声明为友元函数,类似于写在主函数后的子函数应当先声明再调用)
可以把类B声明为类A的友元类,此时类B中的所有函数都是A类的友元函数,都能直接访问A类中的所有成员,但A不能访问B的私有成员,友元关系具有单向性
友元关系也没有传递性,A是B的友元,B是C的友元不能说明A是C的友元
也可单独将B类的一个成员函数声明为A的友元函数,只需要
举例B类中有这样的函数
void func1();
将其在A类中声明为友元只需要加friend和作用域标识,在A类中这样写:
friend void B::func1();
即可将B类中的func1()声明为A类的友元函数
类可以作为另一个类的子类出现,若子对象对应的类的构造函数有参数,则包含子对象的类必须使用表达式的方式先初始化子对象
9.18
x++和++x
m=x++是先把x赋值给m而后x自加
m=++x直接把x自加后赋给m
输出也是一样,输出x++是先输出x之后x再自加
输出++x是直接输出x自加后的值
x++是先运行以x为参数的动作,动作结束后再自加
++x是自加后再做动作
9.21
::也可表示全局作用域符号,当全局变量和函数内的局部变量重名时,在变量名前加入::可表示全局变量
C++中类的数据成员不管是不是私有的,都不能直接在类中进行初始化。因为类就相当于一个数据的模板,是一种自定义组合的新数据类型,而不是一个变量。当程序声明一个类的时候,事实上并没有为程序申请存储空间,只有用这个类定义一个类对象的时候,才申请空间。
普通变量要通过构造函数来对类进行初始化,静态变量在类外部手动进行初始化//??存疑
新建类时可在VS编译器中选择添加类,可生成默认类模板方便编写
引用文件时<>和 ""效果是不一样的
<>表示到编译器默认的存放一些公共头文件的目录去寻找头文件,而找不到自己在工程目录下定义的头文件
""表示先在该工程目录下寻找,如果找不到再去默认公共头文件目录寻找
System("cls");//可实现清屏
9.26
类继承和派生
继承体现重用性,派生体现扩充性
单继承:派生类只继承于一个基类
多继承:派生类继承多个基类
继承方式:
公有派生类public:基类的公有成员和保护成员属性不变,私有成员在派生类内外均不可被访问
私有派生类private:基类中的公有成员和保护成员在私有派生类中转换为私有成员,因此在派生类内部可访问基类公有成员,外部不能访问;另外基类中的private在派生类内外均不可访问
protected保护成员:能够被基类和派生类访问, 但在类外不能被访问
protected保护派生类:基类的公有和保护成员均在派生类中转换为保护成员,私有成员依然不可访问
同名访问规则:在派生类中,可以重写并覆盖与基类同名的成员,同名成员在调用时通过类名加限定运算符::加以区分
派生类创建形式
class derived-class: access-specifier base-class
派生类和基类满足赋值兼容规则则可以相互转换
赋值兼容原则:在公有派生方式下,派生对象可以作为基类对象来使用,派生类对象可以直接赋值给基类对象,基类对象的引用可以引用派生类对象,基类对象的引用可以引用派生类对象,基类对象的指针也可以指向派生类对象,反过来不可以
所有子类对象都是基类的对象,
当把派生类赋值给基类时,同名的成员变量依然沿用基类的内容,而不用派生类中的内容赋值
反过来基类对象不能赋值给派生类,就好比不能将一个大的定义赋值给一个小的定义范围,比如狗属于动物,因此狗的一些基本特性(非同名变量)可以赋值给动物这个大类,反过来所有动物都属于狗是错的,因此基类不能赋值给派生类
派生类构造函数的调用顺序:1先调用基类的构造函数,如果基类中有对象成员则要调用那个对象成员所属类的构造函数
2如果派生类中含有对象成员,则调用派生类自身所含有的对象成员所属类的构造函数
3最后调用派生类自己的构造函数
析构函数的调用顺序正好相反///可以写程序检验调用顺序
多继承默认继承方式为私有继承
多继承的多个基类中可能含有同名成员,因此要加上类名和::加以区分
特殊情况:当一个基类A派生出两个类B1 B2 这两个派生类同时又作为第三个C类多继承的两个基类时,此时A作为C的间接基类,A类中的公有成员在C类中被调用时,仅通过类名A加::不能确定其是来自B1还是B2类,因此可通过A.B1::和A.B2::来区分(但并不是最好方法)
过多层次的多继承会使得程序变得复杂难以维护,因此单继承能解决的问题不建议使用多继承,在C#,JAVA,PHP等语言中均不支持多继承
在多继承时,一个类不能重复成为另一个类的直接基类,但能多次成为间接基类,此时派生类访问基类成员时可能出现二义性
可通过虚基类解决这样的二义性问题,将B1与B2声明为virtual即让B1,B2承诺允许共享它的基类,可使得C在继承B1 B2时仅产生一个间接基类A的拷贝。假设若先声明B1,则A为B1的真基类,B1,B2沿用A在B1的拷贝,A是B2的假基类;反之则B2为真基类,B1为假基类
10.1
编译时的多态:函数重载、运算符重载
运行时的多态:虚函数
运算符重载可用于重定义运算符作用在类类型上的含义,若无运算符重载机制,则定义的Class A,B,C ; A=B+C; 将不能实现
复数的加减运算可用类加运算符重载来实现
重载函数的表示形式为: class operator+(parameter1,parameter2.... )
可用友元函数形式实现运算符重载,将运算符左右两侧的内容作为函数的两个参数
ostream& operator<<(ostream & out, Class obj); 可将<<进行重载令其能够输出类对象,而且可连续使用 ,例如AB为类的两个对象,cout<< A <<B <<endl这样实用是可以的,因为此处<<的两个参数一个是引用的ostream类对象,另一个是class类的对象obj,而返回值又是一个引用的ostream类对象,因此在cout<<A这部分中,cout是ostream类,A是Class类,成立,这部分操作同时返回一个ostream类的引用,因此cout<<A的返回值正好可以作为下一部分<<B的第一个输入参数,B是Class类的对象,为第二个输入参数,最后一个<<endl中运算符正常使用,没有重载
选用引用方式避免了指针空间的多次分配和构造函数的多次调用
ostream& out 为定义一个名字为out的引用型输出流对象
不能重载的运算符有: 作用域操作符:::
条件操作符:?:
点操作符:.
指向成员操作的指针操作符:->*,.*
预处理符号 #
sizeof
.、.*运算符不能重载是为了保证访问成员的功能不能被改变,域运算符合sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。
10.3
malloc从堆中分配出一定的动态内存,返回这块内存的首地址
malloc常常要和free配对使用,malloc分配的地址不能随意忽略,要么把它的地址保存下来方便访问,要么free掉
注意在free的过程中容易出问题,free的参数为指向待释放的内存空间首地址的指针,即malloc的返回值,记为指针p,在malloc分配后我们常常移动p来访问其中的内存空间,因此在后面执行free命令时,p常常不在空间的首地址,这就容易出问题,最好的方法是再malloc使用时就再p之外多备份一个指针q,q与p一样指向内存空间首地址,不对q进行操作,q只作为后来作为free的参数确保正确释放内存
另外注意free之后要加上p=NULL 这是因为free之后内存空间已经被系统收回,被系统占用,此时p指针如果还指向原内存空间中并访问或者修改的话可能会影响到系统的其他进程,导致故障,因此要将p指针指为空
malloc返回的指针类型为void *, void *代表着这个指针指向的目标对象没有数据类型,需要进行强制类型转换为其赋予数据类型。
11.7
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
11.14
句柄类型 handle
HANDLE就是PVOID,也就是无类型指针(不指向任何类型数据的指针)句柄其实就是指针,但是他和指针最大的不同是,给你一个指针,你可以通过这个指针做任何事情,也许是做好事,也许是通过这个指针破坏内存,句柄就没有这个缺点,通过句柄,你能干一些windows让你干的事情,没有了指针的坏处 (具体如何去做?我也不知道)在windows程序设计中,句柄是无法精确定义的术语,但是在程序设计中,句柄无所不在,窗口有窗口的句柄HWND,线程和进程也有句柄HANDLE,甚至有人把套接字也称为句柄,简而言之,句柄是处理对象的一个接口,你可以通过句柄去操作程序中所涉及的对象。
为了解决此类问题,现代CPU引入了 MMU(Memory Management Unit 内存管理单元)。
MMU 的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由 MMU 负责将虚址映射为物理地址。MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。
内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。
在上文中提到,虚拟地址与物理地址需要通过映射,才能使CPU正常工作。
而映射就需要存储映射表。在现代CPU架构中,映射关系通常被存储在物理内存上一个被称之为页表(page table)的地方。
进一步优化,引入TLB(Translation lookaside buffer,页表寄存器缓冲)
由上一节可知,页表是被存储在内存中的。我们知道CPU通过总线访问内存,肯定慢于直接访问寄存器的。
为了进一步优化性能,现代CPU架构引入了TLB,用来缓存一部分经常访问的页表内容。
事实上,Windows内存管理器管理的其实都是句柄,通过句柄来管理指针
11.16
复习
由类实例化形成对象
属性往往表示对象本身的静态的特质,比如一些数据
方法往往表示动态特质比如函数