0%

C&Cpp杂谈(持续更新)

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 gcd(int, int);
int main(){
printf("The greatest common divisor is %d\n", gcd(100, 60));
return 0;
}
//函数定义
int gcd(int a, int b){
//若a<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); // 因为func1函数是在main函数之后创建的,因此需要函数原型声明

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); // radius 就只有在括号中才有效
// 如果后面没有跟“ ; ”,而是跟了“ {} ”,那么就不是函数声明,而是函数定义了,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,而唯一的那个定义声明中,省略存储类说明符

在声明定义时,定义数组如下:

1
int G_glob[100];

在另一个文件中引用声明如下:

1
int * G_glob;

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
};
// a占第一字节的4位,后4位填0表示不使用

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);
}
//void sayHello(){
// printf("Triangle\n");
//}
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 /* Triangle中没有覆写sayHello,所以还是执行父类的sayHello */
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)); /* ShapeP就是去掉虚函数的Shape */
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
  • 很明显,编译器多分配了8字节,用于存储虚表头指针

内存布局如下:

  • 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);
}
//void sayHello(){
// printf("Triangle\n");
//}
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不是虚函数,直接赋值函数指针 */
BAR_FUN bar_fun = (BAR_FUN)(&Shape::bar);

/* 获取vtable地址 */
ADRESS *vtable_addr = *((ADRESS**)(shape));

/* 通过vtable赋值函数指针 */
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 没有任何差别