字符指针(Character Pointer)char*
是指向字符数据的指针变量。
使用实例:
int main() {
char ch = 'w'; // 声明一个字符变量ch并赋值为 'w'
char *pc = &ch; // 声明一个字符指针pc并将其指向ch的地址
*pc = 'w'; // 通过指针pc修改ch的值为 'w'
char arr[] = "abcdef"; // 声明一个字符数组arr并初始化为字符串 "abcdef"
char* p = arr; // 声明一个字符指针p,将其指向数组arr的首地址
return 0;
}
还有一种使用方式如下:
#include
int main() {
const char *pstr = "hello world."; // 声明一个指向常量字符的指针pstr,并将其指向字符串常量"hello wrold."
printf("%s\n", pstr); // 输出字符串"hello world."
*pstr = 'w'; // 尝试修改常量字符串,会导致编译错误,因为常量字符串不可修改
printf("%c\n", *pstr); // 尝试访问常量字符串的第一个字符,不会发生修改,输出仍为原始字符'h'
return 0;
}
通过指针修改常量字符串的操作是非法的,会导致编译错误。常量字符串应该被视为只读数据。如果需要修改字符串内容,应该使用字符数组而不是指向常量字符的指针。
代码 const char* pstr = "hello world.";
特别容易让同学以为是把字符串 hello world
放到字符指针 pstr
里了,但是/本质是把字符串 hello world.
首字符的地址放到了pstr
中。
下面的结果是什么?
#include
int main() {
char str1[] = "hello world.";
char str2[] = "hello world.";
const char *str3 = "hello world.";
const char *str4 = "hello world.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
//str1 and str2 are not same
//str3 and str4 are same
str1
和 str2
是字符数组,它们包含了相同的字符串 “hello world.”。str3
和 str4
是指向常量字符的指针,它们也指向相同的字符串 “hello world.”。然后,程序使用条件语句比较了这四个变量的指针值,并输出比较结果:
str1 == str2
比较,它们是两个不同的字符数组,所以输出 “str1 and str2 are not same”。str3 == str4
比较,它们指向相同的常量字符,所以输出 “str3 and str4 are same”。尽管 str1
和 str2
的内容相同,但它们是两个不同的数组,因此它们在内存中的地址也不同。而 str3
和 str4
是指向相同字符串常量的指针,它们指向的地址相同。
如果想比较字符串的内容而不是地址,应该使用字符串比较函数
strcmp()
,例如strcmp(str1, str2)
。
在C语言中,指针数组是指一个数组,其元素都是指针类型的变量。指针数组可以存储指向不同类型对象的指针。
那么指针数组是指针还是数组?
是数组。是存放指针的数组。
例如:
int main() {
//整型数组-存放整型的数组
int arr[10];
//字符数组-存放字符的数组
char arr2[5];
//指针数组-存放指针的数组
int *arr3[5]; //存放整型指针的数组
char *arr4[6];//存放字符指针的数组
return 0;
}
例如:
#include
int main() {
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int e = 50;
int *arr3[5] = {&a, &b, &c, &d, &e};//存放整型指针的数组
int i = 0;
for (i = 0; i < 5; i++) {
printf("%d ", *(arr3[i]));
}
return 0;
}
指针数组使用场景:用一个一维数组模拟二维数组
#include
int main() {
//用一维数组模拟一个二维数组
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
int arr4[] = {4, 5, 6, 7, 8};
int *arr[4] = {arr1, arr2, arr3, arr4};
int i = 0;
for (i = 0; i < 4; i++) {
int j = 0;
for (j = 0; j < 5; j++) {
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
//int i = 0;
//for (i = 0; i < 4; i++)
//{
// int j = 0;
// for (j = 0; j < 5; j++)
// {
// printf("%d ", arr[i][j]);
// }
// printf("\n");
//}
return 0;
}
输出结果:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉: 整型指针: int * pint
; 能够指向整型数据的指针。
浮点型指针: float * pf
; 能够指向浮点型数据的指针。 那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
解释:
int (*p)[10]
;
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于号的,所以必须加上()来保证p先和结合。
int main(){
int arr[10] = {1,2,3,4,5};
int (* pa)[10] = &arr;//取出的是数组的地址存放到pa中,pa是数组指针变量
char arr[5];
char (*p1)[5] = &arr;
}
对于下面的数组:
int arr[10];
arr
和 &arr
分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥? 我们看一段代码:
#include
int main() {
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
运行结果如下:
000000000061FDF0
000000000061FDF0
可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:
#include
int main() {
int arr[10] = {0};
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr+1= %p\n", &arr + 1);
return 0;
}
输出结果如下:
arr = 000000000061FDF0
&arr= 000000000061FDF0
arr+1 = 000000000061FDF4
&arr+1= 000000000061FE18
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr
表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中&arr
的类型是: int(*)[10]
,是一种数组指针类型数组的地址+1,跳过整个数组的大小,所以&arr+1
相对于&arr
的差值是40.
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
实例:
#include
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
一个数组指针的使用:
#include
void print1(int *arr, int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
//*(arr + i) 表示指针arr偏移i个元素后所指向的值。
printf("%d ", *(arr + i));
}
printf("\n");
}
void print2(int (*p)[10], int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
//int (*p)[10]是数组指针,(*p)[i] 表示指针p所指向的数组的第i个元素的值。
printf("%d ", (*p)[i]); //*p = arr;
}
printf("\n");
}
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
print1(arr, sz); //1 2 3 4 5 6 7 8 9 10
print2(&arr, sz); //1 2 3 4 5 6 7 8 9 10
return 0;
}
print1
函数的参数是一个整型指针,可以传递任意大小的整型数组给它进行打印。而 print2
函数的参数是一个指向包含10个整数的数组的指针,因此只能传递大小为10的整型数组给它进行打印。
这两种写法都可以用于打印整型数组的元素,选择使用哪种方式取决于你的需求和所操作的数组的类型和大小。
继续:
void print3(int arr[3][5], int r, int c) {
int i = 0;
for (i = 0; i < r; i++) {
int j = 0;
for (j = 0; j < c; j++) {
//arr[i][j] 表示二维数组arr的第i行、第j列的元素的值。
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print4(int (*p)[5], int r, int c) {
//函数接受一个指向包含5个整数的数组指针p
int i = 0;
for (i = 0; i < r; i++) {
int j = 0;
for (j = 0; j < c; j++) {
//*(*(p + i) + j) 表示指针p所指向的二维数组的第i行、第j列的元素的值。
//p为指针 p+i表示一维数组
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main() {
int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}};
print3(arr, 3, 5);
print4(arr, 3, 5);
return 0;
}
解析 *(*(p + i) + j)
:
(p + i)
:首先,p
是一个指向包含5个整数的数组的指针。通过 p + i
运算,指针 p
偏移了 i
个整数大小的字节,即指向了二维数组的第 i
行。*(p + i)
: 在上一步中,p
偏移了 i
行,所以 *(p + i)
表示指针 p
所指向的二维数组的第 i
行的首个元素。这是一个指向整数的指针。(*(p + i) + j)
: 在上一步的基础上,j
是用于偏移列的索引。*(p + i) + j
表示指针 *(p + i)
所指向的一维数组中,偏移了 j
个整数大小的字节,即指向了二维数组中第 i
行、第 j
列的元素。*(*(p + i) + j)
: 最后,*(*(p + i) + j)
使用指针解引用操作符 *
,获取指针 (*(p + i) + j)
所指向的整数值。这就是二维数组中第 i
行、第 j
列的元素值。学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
int arr[5];
这是一个包含5个整数的数组,名为arr
。int *parr1[10];
这是一个包含10个指向整数的指针的数组,名为parr1
。它可以存储10个整数的地址。int (*parr2)[10];
这是一个指向包含10个整数的数组的指针,名为parr2
。它指向一个具有10个整数的数组。int (*parr3[10])[5];
这是一个包含10个指向包含5个整数数组的指针的数组,名为parr3
。它可以存储10个指向具有5个整数的数组的指针。让我们分解int (*parr3[10])[5]
并解释其含义:
parr3
开始,我们知道这是一个数组。[10]
,表示parr3
是一个包含10个元素的数组。*
,表示数组的元素是指针。[5]
,表示指针指向的是包含5个整数的数组。int
,表示数组中的整数类型。因此,int (*parr3[10])[5]
可以解读为:parr3
是一个包含10个元素的数组,每个元素都是指向包含5个整数的数组的指针。
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
#include
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int main() {
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
上述代码中都OK。arr[10]
等价于*arr
,[]中的值可以省略。 *arr[20]
等价于**arr
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok? err
{}
void test(int arr[][5])//ok?
{}
int main() {
int arr[3][5] = {0};
test(arr);
}
总结:
二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。
void test(int *arr)//ok?
{}
void test(int *arr[5])//ok? err
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok? err
{}
int main() {
int arr[3][5] = {0};
test(arr);
}
函数 test(int *arr[5])
是错误的,因为它声明了一个包含 5 个指向整数的指针的数组,而不是一个指向整数的指针。函数 test(int **arr)
也是错误的,因为它声明了一个指向指针的指针,而不是一个指向整数或整数数组的指针。在主函数中,参数 int *arr
的正确函数声明应该是 test(int (*arr)[5])
,因为它是一个包含 5 个整数的数组的指针 。
#include
void print(int *p, int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", *(p + i));//1 2 3 4 5 6 7 8 9 10
}
}
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int *p)
{}
//test1函数能接收什么参数? int型一维数组,和int型变量的地址
void test2(char* p)
{}
//test2函数能接收什么参数? char型一维数组,和char型变量的地址
#include
void test(int **ptr) {
printf("num = %d\n", **ptr);
}
int main() {
int n = 10;
int *p = &n;
int **pp = &p;
test(pp);//num = 10
test(&p);//num = 10
return 0;
}
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p) {
}
int main() {
char c = 'b';
char *pc = &c;
char **ppc = &pc;
char *arr[10];
test(&pc); //pc是一个一级指针,&pc即二级指针
test(ppc); //ppc是一个二级指针
test(arr);//Ok?
return 0;
}
数组名本身也可以被解释为指向数组首元素的指针。对于一个char*
类型的数组arr[10]
,arr
可以被解释为指向arr[0]
的指针,而arr[0]
又是一个char*
类型的指针。
因此,当调用test(arr)
时,实际上将指向arr[0]
的指针传递给了test
函数。在test
函数内部,参数p
的类型是char**
,它可以接收指向char*
类型的指针。因此,test(arr)
是合法的。
首先看一段代码:
#include
void test() {
printf("hehe\n");
}
int main() {
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的结果:
0000000000401550
0000000000401550
输出的是两个地址,这两个地址是 test 函数的地址。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test() {
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
函数指针是指向函数的指针变量。它可以用于在程序运行时动态地选择调用不同的函数,或者将函数作为参数传递给其他函数。
函数指针的声明:
return_type (*pointer_name)(parameter_list);
其中,return_type
是函数的返回类型,pointer_name
是指针变量的名称,parameter_list
是函数的参数列表。例如,以下是一个指向返回整数类型、接受两个整数参数的函数指针的声明:
int (*sum_ptr)(int, int);
函数指针的赋值:
函数指针可以通过将函数的名称赋值给指针变量来进行初始化。例如,假设有以下函数定义:
int sum(int a, int b) {
return a + b;
}
可以将该函数赋值给函数指针:
sum_ptr = sum;
使用函数指针调用函数: 使用函数指针调用函数与直接调用函数的语法相似,只需将指针变量后面加上参数列表即可。例如:
int result = sum_ptr(3, 4);
实例1:
#include
int Add(int x, int y) {
return x + y;
}
int main() {
//pf就是函数指针变量
int (*pf)(int x, int y) = Add; //Add和&Add都是函数的地址,没有区别,类似于数组名和&数组名
int sum = (*pf)(3, 5); //等价于int sum = Add(3, 5);
printf("%d\n", sum); //8
return 0;
}
实例2:
#include
int test(const char *str, double d) {
}
int main() {
int (*pt1)(const char *, double) = &test;
int (*pt2)(const char *str, double d) = &test;
return 0;
}
阅读两段有趣的代码:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
(*(void (*)())0)();
把0直接转换成一个void (*)()的函数指针,然后去调用0地址处的函数,函数的参数为无参
void (*signal(int , void(*)(int)))(int);
是一次函数声明
声明的函数叫:signal
signal函数的第一个参数是int类型的
signal函数的第二个参数是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void
signal函数的返回类型也是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void
代码2太复杂,如何简化:
typedef void(*pfun_t)(int); //pfun_t 是一个函数指针,参数是int,返回类型是void
pfun_t signal(int, pfun_t);
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组
比如:
int *arr[10];
//数组的每个元素是int*
函数指针数组中的每个元素都是一个函数指针。函数指针是指向函数的指针变量,可以用来调用该函数。
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])(); //函数指针数组,指向一个无参函数,返回值为int
int *parr2[10]();
int (*)() parr3[10];
答案是:parr1
parr1
先和 []
结合,说明 parr1
是数组,数组的内容是什么呢?
是 int (*)()
类型的函数指针。
函数指针数组的用途:转移表
例子:(计算器)
#include
int Add(int x, int y) {
return x + y;
}
int Sub(int x, int y) {
return x - y;
}
int Mul(int x, int y) {
return x * y;
}
int Div(int x, int y) {
return x / y;
}
void menu() {
printf("***************************\n");
printf("***** 1.add 2. sub ****\n");
printf("***** 3.mul 4. div ****\n");
printf("***** 0.exit ****\n");
printf("***************************\n");
}
int main() {
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {
case 1:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
}
使用函数指针数组简化代码:
#include
int Add(int x, int y) {
return x + y;
}
int Sub(int x, int y) {
return x - y;
}
int Mul(int x, int y) {
return x * y;
}
int Div(int x, int y) {
return x / y;
}
void menu() {
printf("***************************\n");
printf("***** 1.add 2. sub ****\n");
printf("***** 3.mul 4. div ****\n");
printf("***** 0.exit ****\n");
printf("***************************\n");
}
int main() {
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//函数指针数组 - 转移表
int (*pfArr[])(int, int) = {0, Add, Sub, Mul, Div};
do {
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0) {
printf("退出计算器\n");
break;
}
if (input >= 1 && input <= 4) {
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
} else {
printf("选择错误\n");
}
} while (input);
}
指向函数指针数组的指针是一个指针,它指向函数指针数组的起始地址。可以使用这个指针来访问函数指针数组中的元素,以及通过函数指针调用相应的函数。
下面是一个简单的示例,展示了如何声明和使用指向函数指针数组的指针:
#include
void test(const char* str){
printf("%s\n", str);
}
int main(){
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
// 函数指针的声明
typedef void (*FuncPtr)(int);
// 函数定义
void func1(int num) {
printf("func1: %d\n", num);
}
void func2(int num) {
printf("func2: %d\n", num);
}
void func3(int num) {
printf("func3: %d\n", num);
}
int main() {
// 函数指针数组的定义和初始化
FuncPtr funcs[] = { func1, func2, func3 };
// 指向函数指针数组的指针
FuncPtr (*ptrToFuncArray)[3] = &funcs;
// 通过指针访问函数指针数组的元素,并调用相应的函数
(*ptrToFuncArray)[0](10); // 调用 func1
(*ptrToFuncArray)[1](20); // 调用 func2
(*ptrToFuncArray)[2](30); // 调用 func3
return 0;
}
回调函数是一种函数指针的使用方式,它允许将一个函数作为参数传递给另一个函数,并在需要的时候调用该函数。回调函数提供了一种灵活的机制,使得代码可以根据特定的条件或事件来执行不同的行为。
#include
// 回调函数类型的定义
typedef void (*CallbackFunc)(int);
// 执行回调函数的函数
void performCallback(CallbackFunc callback, int value) {
// 调用传递进来的回调函数
callback(value);
}
// 回调函数1
void callback1(int value) {
printf("Callback 1: %d\n", value);
}
// 回调函数2
void callback2(int value) {
printf("Callback 2: %d\n", value);
}
int main() {
int data = 10;
// 使用回调函数1进行操作
performCallback(callback1, data);
// 使用回调函数2进行操作
performCallback(callback2, data);
return 0;
}
输出结果如下:
Callback 1: 10
Callback 2: 10
我们首先定义了一个回调函数类型 CallbackFunc
,它是一个函数指针类型,可以指向一个带有一个 int
参数和 void
返回类型的函数。
然后,我们定义了一个名为 performCallback
的函数,它接受一个回调函数作为参数,并在需要的时候调用该函数,传递给它一个值。
接下来,我们定义了两个回调函数 callback1
和 callback2
,它们都符合 CallbackFunc
类型的函数签名。
在 main
函数中,我们定义了一个整数变量 data
,然后使用 performCallback
函数两次,分别传递不同的回调函数和 data
值。这样就实现了在不同情况下执行不同回调函数的功能。
当程序运行时,会依次调用 performCallback
函数,并根据传递的回调函数打印相应的信息。
C语言中qsort函数也是通过函数指针回调函数实现的
#include
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void *p1, const void *p2) {
return (*(int *) p1 - *(int *) p2);
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
用冒泡排序模拟实现下回调函数方式:
#include
int cmp_int(const void *e1, const void *e2) {
return (*(int *) e1 - *(int *) e2);
}
//char类型1字节,适用于任何类型
void Swap(char *buf1, char *buf2, int width) {
int i = 0;
for (i = 0; i < width; i++) {
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void *base, int sz, int width, int (*cmp)(const void *e1, const void *e2)) {
int i = 0;
//趟数
for (i = 0; i < sz - 1; i++) {
//一趟冒泡排序的过程
int j = 0;
for (j = 0; j < sz - 1 - i; j++) {
if (cmp((char *) base + j * width, (char *) base + (j + 1) * width) > 0) {
//交换
Swap((char *) base + j * width, (char *) base + (j + 1) * width, width);
}
}
}
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble_sort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp_int);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}