C语言-作用域 作用域简析
所谓作用域,就是变量的有效范围,即变量可以在哪个范围以内使用
// 有些变量可以在所有代码文件中使用,有些变量只能在当前的文件中使用,有些变量只能在函数内部使用,有些变量只能在 for 循环内部使用
变量的作用域由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的
C语言编译器可以确认四种不同类型的作用域:
代码块作用域(代码块是{}之间的一段代码)
文件作用域
原型作用域
函数作用域(局部变量)
参考:
C语言总结之变量的种类
C语言 作用域
常见的代码块作用域
一,如:“if(){}”,“while(){}”,“switch(){ case 1:{}……}”等各种情况的{}即块作用作用域,在{}中定义的变量,作用域仅限定在{}中
换句话说,在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了
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 #include <stdio.h> int gcd (int a, int b) ; int main () { printf ("The greatest common divisor is %d\n" , gcd(100 , 60 )); return 0 ; } int gcd (int a, int b) { if (a < b){ int temp1 = a; a = b; b = temp1; } while (b!=0 ){ int temp2 = b; b = a % b; a = temp2; } return a; }
“temp1”的作用域是 if 内部,“temp2”的作用域是 while 内部
二,for循环中定义的变量,作用作用域仅限于for循环
三,单独的代码块也可以成立
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { int n = 22 ; { int n = 40 ; printf ("block n: %d\n" , n); } printf ("main n: %d\n" , n); return 0 ; }
这里有两个 n,它们位于不同的作用域,不会产生命名冲突
// { } 的作用域比 main() 更小
参考: C语言块级变量:在代码块内部定义的变量
文件作用域简析
简单来说,就是在函数外声明的作用域,其名称从声明的地方开始,到该程序的结尾都是通用的
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 #include <stdio.h> #define NUMBER 5 int v[NUMBER]; int func1 (void ) ; int main (void ) { extern int v[]; int i; puts ("please input the scores." ); for (i = 0 ; i < NUMBER; i++) { printf ("v[%d] = " , i); scanf ("%d" , &v[i]); } printf ("the max : %d\n" , func1()); return 0 ; } int func1 (void ) { extern int v[]; ## 非定义声明,可省略 int i, max = v[0 ]; for (i = 0 ; i < NUMBER; i++) { if (v[i] > max) max = v[i]; } return max; }
从“int func1(void)”开始到文件结束,都是“func1”的文本作用域,所以要在“main”前写入“int func1(void);”,使其在作用域中
// 函数定义的“int func1(void)”也有声明的功效,为 定义声明 ,而其他的都是 引用声明
参考:C语言中的文件作用域、函数原型声明、定义声明和非定义声明
原型作用域简析
函数原型作用域只对于函数原型声明的形式参数有意义(其它变量声明之类都在块作用域)
其作用域始于“(”,结束于“)”
1 2 double Area (double radius) ;
函数原型: 即函数声明,给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息
作用域的层级关系
每个C语言程序都包含了多个作用域,不同的作用域中可以出现同名的变量,C语言会按照从小到大 的顺序,一层一层地去父级作用域中查找变量,如果在最顶层的全局作用域中还未找到这个变量,那么就会报错
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 #include <stdio.h> int m = 13 ;int n = 10 ;void func1 () { int n = 20 ; { int n = 822 ; printf ("block1 n: %d\n" , n); } printf ("func1 n: %d\n" , n); } void func2 (int n) { for (int i=0 ; i<10 ; i++){ if (i % 5 == 0 ){ printf ("if m: %d\n" , m); }else { int n = i % 4 ; if (n<2 && n>0 ){ printf ("else m: %d\n" , m); } } } printf ("func2 n: %d\n" , n); } void func3 () { printf ("func3 n: %d\n" , n); } int main () { int n = 30 ; func1(); func2(n); func3(); printf ("main n: %d\n" , n); return 0 ; }
C语言-存储类说明符 C语言的存储类说明符有以下几个:
Auto:只在 块内 变量声明中被允许, 表示变量具有本地生存期
Extern:出现在 顶层 或 块的外部 的变量函数与变量声明中,表示声明的对象具有静态生存期, 连接程序知道其名字
Static:可以放在 函数与变量声明中 ,在函数定义时,只用于指定函数名,而不将函数导出到链接程序,在函数声明中,表示其后边会有定义声明的函数,存储类型static,在数据声明中,总是表示定义的声明不导出到连接程序
C99中规定:所有顶层的默认存储类标志符都是extern
在C语言中一个人为的规范:
1 在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern 修饰符,反之,则必须显示使用extern 修饰符
C语言-定义声明&引用声明 定义声明:在声明后,立即进行定义或初始化(如:“ fun( int a ){ } ”,“ int a = 1 ”)
引用声明:其他的声明都是引用声明
// 这个只是“初始化语句模型”中的定义方式(为了方便理解)
为了区分定义声明和引用声明,C语言定义了几种模型:
初始化语句模型:顶层声明中,如果存在初始化语句,表示这个声明是定义声明,其他声明是引用声明
省略存储类型模型:所有引用声明要显示的包括存储类extern,而唯一的那个定义声明中,省略存储类说明符
在声明定义时,定义数组如下:
在另一个文件中引用声明如下:
C语言 -“.h” 文件的作用 在一个C语言程序中有千千万万个函数(例如:“printf”,“read”……),为了它们的正常使用(文件作用域可以包含其“作用点”),需要事先写明函数声明
这些函数声明并没有写入“.c”文件中,而是写入了“.h”文件中
另外,全局变量也可以写在“.h”文件中,不同的“.h”文件可以写入相同名称的全局变量
参考: C语言中的.h文件的作用
C语言-位域 有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位
所谓 “位域” 是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数
1 2 3 4 5 6 7 struct bs { int a:8 ; int b:2 ; int c:6 ; }data;
说明data为bs变量,a占8位,b占2位,c占6位,共占两个字节(这里假定int类型长度为16位,2字节,通常int都是32位,4字节)
1.一个位域必须存储在同一个单元中,不能跨两个单元,如一个单元所剩空间不够存放另一位域时,应从下一单元起存放该位域
2.可以人为控制“启用&关闭”位域
1 2 3 4 5 6 7 8 struct as { unsigned a:4 unsigned :0 unsigned b:4 unsigned c:4 };
3.位域可以无位域名,这时它只用来作填充或调整位置
1 2 3 4 5 6 7 struct bs { int a:1 int :2 int b:3 int c:2 };
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 #include <stdio.h> #include <string.h> class Shape { protected : int width,height; public : Shape (int a=0 ,int b=0 ){ width = a; height = b; } virtual int area () { printf ("Shape class area: " ); return printf ("%d\n" ,0 ); } virtual void sayHello () { printf ("Shape\n" ); } void bar () { printf ("bar fun width:%d\n" ,width); } }; class Rectangle :public Shape{ public : Rectangle (int a=0 ,int b=0 ):Shape (a,b){} int area () { printf ("Rectangle class area: " ); return printf ("%d\n" ,width*height); } void sayHello () { printf ("Rectangle\n" ); } virtual void fun1 () { printf ("fun1\n" ); } }; class Triangle :public Shape{ public : Triangle (int a=0 ,int b=0 ):Shape (a,b){} int area () { printf ("Triangle class area: " ); return printf ("%d\n" ,width*height/2 ); } virtual void fun2 () { printf ("fun2\n" ); } }; void test (Shape* shape) { shape->bar (); shape->area (); shape->sayHello (); } int main () { Shape* shape = new Rectangle (3 ,4 ); test (shape); delete shape; shape = new Triangle (3 ,4 ); test (shape); delete shape; shape = new Shape (3 ,4 ); test (shape); delete shape; }
定义了一个 Shape 类,Rectangle 类和 Triangle 类都继承 Shape 类
在 Shape 类中:area 和 sayHello 都是虚函数(用 virtual 进行修饰)
结果:
1 2 3 4 5 6 7 8 9 10 11 ➜ exp g++ test.cpp -o test -g -no-pie ➜ exp ./test bar fun width:3 Rectangle class area : 12 Rectangle bar fun width:3 Triangle class area : 6 Shape bar fun width:3 Shape class area : 0 Shape
虚函数可以被子类覆写(名称&格式必须相同),调用时优先调用自己的虚函数
这就是多态,使用虚函数使子类可以覆写父类,提高了函数的灵活性
Cpp-动态绑定 一般情况下,在编译期间(包括链接期间)就能完成符号决议(为符号绑定相应的地址),不用等到程序执行时再进行额外的操作,这称为静态绑定,如果编译期间不能完成符号决议,就必须在程序执行期间完成,这称为动态绑定
非虚成员函数属于静态绑定:编译器在编译期间,根据指针(或对象)的类型完成了绑定
调用虚函数时,就会发生动态绑定,所谓动态绑定,就是在运行时,虚函数会 根据绑定对象的实际类型,选择调用函数的版本 (因为 cpp 的多态机制,我们无法在编辑阶段完成符号决议,因为不清楚该虚函数是A类,B类,还是C类)
例如:我们不清楚符号 area 会绑定 Shape 中的代码地址,还是 Triangle 中的代码地址,或者是 Rectangle 中的代码地址
如果一个类包含了虚函数,那么在创建对象时会额外增加一张表,表中的每一项都是虚函数的入口地址,这张表就是虚函数表,也称为 vtable
可以认为虚函数表是一个数组,为了把对象和虚函数表关联起来,编译器会在对象中安插一个指针,指向虚函数表的起始位置,这个指针就是 VPTR 指针(在以后的 pwn 中会遇到)
Cpp-虚表 为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术,这个技术的核心是虚函数表
我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权,所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表
我们来看以下的代码:(紧接上面的例子)
1 2 3 4 printf ("ShapeP :%ld\n" ,sizeof (ShapeP)); printf ("Shape :%ld\n" ,sizeof (Shape));printf ("Rectangle: %ld\n" ,sizeof (Rectangle));printf ("Triangle: %ld\n" ,sizeof (Triangle));
Shape,Rectangle,Triangle,都应该有虚表
ShapeP 应该没有虚表
1 2 3 4 ShapeP :8 Shape :16 Rectangle: 16 Triangle: 16
内存布局如下:
Shape:定义了虚函数 area,sayHello
Rectangle:覆写了虚函数 area,sayHello,定义了虚函数 fun1
Triangle:覆写了虚函数 area,继承了虚函数 sayHello,定义了虚函数 fun2
我们可以用另一种方式进行验证:
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 #include <stdio.h> #include <string.h> class Shape { protected : int width,height; public : Shape (int a=0 ,int b=0 ){ width = a; height = b; } virtual int area () { printf ("Shape class area: " ); return printf ("%d\n" ,0 ); } virtual void sayHello () { printf ("Shape\n" ); } void bar () { printf ("bar fun width:%d\n" ,width); } }; class Rectangle :public Shape{ public : Rectangle (int a=0 ,int b=0 ):Shape (a,b){} int area () { printf ("Rectangle class area: " ); return printf ("%d\n" ,width*height); } void sayHello () { printf ("Rectangle\n" ); } virtual void fun1 () { printf ("fun1\n" ); } }; class Triangle :public Shape{ public : Triangle (int a=0 ,int b=0 ):Shape (a,b){} int area () { printf ("Triangle class area: " ); return printf ("%d\n" ,width*height/2 ); } virtual void fun2 () { printf ("fun2\n" ); } }; void test (Shape* shape) { shape->bar (); shape->area (); shape->sayHello (); } void test2 (Shape* shape) { typedef void (*BAR_FUN) (void *) ; typedef int (*AREA_FUN) (void *) ; typedef void (*SAYHELLO_FUN) (void *) ; typedef void * (ADRESS); BAR_FUN bar_fun = (BAR_FUN)(&Shape::bar); ADRESS *vtable_addr = *((ADRESS**)(shape)); AREA_FUN area_fun = (AREA_FUN)(*vtable_addr); SAYHELLO_FUN sayhello_fun = (SAYHELLO_FUN)*(vtable_addr+1 ); bar_fun ((void *)shape); area_fun ((void *)shape); sayhello_fun ((void *)shape); } int main () { Shape* shape = new Rectangle (3 ,4 ); printf ("\n------\n" ); test (shape); test2 (shape); printf ("------\n\n" ); delete shape; shape = new Triangle (3 ,4 ); printf ("\n------\n" ); test (shape); test2 (shape); printf ("------\n\n" ); delete shape; shape = new Shape (3 ,4 ); printf ("\n------\n" ); test (shape); test2 (shape); printf ("------\n\n" ); delete shape; }
test2 和 test 完全不同
test2 没有采用 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 ➜ exp g++ test.cpp -o test -g -no-pie ➜ exp ./test ------ bar fun width:3 Rectangle class area : 12 Rectangle bar fun width:3 Rectangle class area : 12 Rectangle ------ ------ bar fun width:3 Triangle class area : 6 Shape bar fun width:3 Triangle class area : 6 Shape ------ ------ bar fun width:3 Shape class area : 0 Shape bar fun width:3 Shape class area : 0 Shape ------
可以正常执行,test 和 test2 没有任何差别