GNU C

文章详细介绍了C语言中的数组,包括一维数组和二维数组的初始化及使用,特别提到了零长度数组和变长数组。接着讨论了指针的概念、定义、赋值以及与数组的关系,并深入到多级指针和函数指针的使用。此外,还涵盖了预处理指令如宏定义和头文件包含,以及常用的库函数如sizeof、printf和scanf。文章最后提及了函数、递归、变量的作用域和存储方式,强调了const修饰符的用法。
摘要由CSDN通过智能技术生成

目录

一、数组

1、一维数组

完全初始化int a[5]={1,2,3,4,5};

不完全初始化int a[5]={1,2}

完全不初始化”,int a[5]

2、二维数组

完全初始化

不完全初始化

二、指针

1、变量的访问方式:

2、指针变量的定义:

3、指针变量的赋值:

4、指针变量的运算

5、指针与数组

6、多级指针

7、函数指针

函数指针的定义方式:

三、常用库函数

1、sizeof

2、printf与scanf

输出控制符

转义符

scanf缓冲

四、预处理指令

1、宏定义: #define

2、# include 

五、函数

六、递归

七、变量的作用域、存储方式、修饰符

1、变量按作用域可分为

2、按存储方式可分为

3、const

const int *p=&a;

int * const p=&a;

const int * const p=&a;


一、数组

GNU C 支持变长数组和零长度数组

零长度数组经常以变长结构体的形式,在某些特殊的应用场合使用。

struct buffer{
    int len;  //用于表示边长结构体的内存长度
    int a[0];
}

int main(void)
{
    struct buffer *buf;
    buf = ( struct buffer *)malloc(sizeof(struct buffer)+20);   //20是边长结构体的真实长度
    buf->len = 2-;
    strcp(buf->a, "hello");  //通过buf->a访问该内存地址
    puts(buf->a);

    free(buf);
    return 0;
}

1、一维数组

int a[5]
数组名a除了表示该数组之外,还表示该数组的首地址
方括号中的常量表达式可以是“数字常量表达式”,也可以是“符号常量表达式”。但不管是什么表达式,必须是常量,绝对不能是变量。


完全初始化int a[5]={1,2,3,4,5};

只有在定义数组的同时才可以整体赋值,其他情况下都不能整体赋值
此时可以不指定数组的长度

不完全初始化int a[5]={1,2}

只给前面两个元素a[0]、a[1]初始化,没有被初始化的元素自动为0
大括号中什么都不写,那就是极其严重的语法错误。大括号中最少要写一个数

指定初始化

int b[100] = { [10] =1,[30] = 2};

int b[100] = { [10 ... 30] =1,[50 ... 60 = 2};   //索引范围初始化

完全不初始化”,int a[5]

不初始化,那么各个元素的值就不是0了,所有元素中都是垃圾值-858993460。

2、二维数组

完全初始化

int a[2][3]={{1,2,3},{4,5,6}};
int a[2][3]={{1,2,3,4,5,6}};


此时可以不指定第一维的数组的长度,但第二维的长度不能省

不完全初始化

int [2][3]={{1,2,},{4}};

二、指针

1、变量的访问方式:

  • 直接访问:程序中一般是通过变量名来对内存单元进行存取操作的。其实程序经过编译以后已经将变量名转换为变量的地址,地址就是内存单元的编号,对变量值的存取都是通过地址进行的。这种按变量地址存取变量的方式称为直接访问方式。
  • 间接访问:变量中存放的是另一个变量的地址。一个变量的地址就称为该变量的指针,指针就是地址,一个变量专门用来存放另一个变量的地址,那么就称它为“指针变量”。通过指针变量访问变量称为间接访问方式

2、指针变量的定义:

基类型 *指针变量名;
int *i
i变量的数据类型是int *型,即存放int变量地址的类型。int和*加起来才是变量i的类型

3、指针变量的赋值:

j = &i;

&是取地址运算符:与scanf中的&是一样的概念;因为j是定义成指针型变量,所以j中只能存放变量的地址,所以变量i前一定要加&。指针变量中只能存放地址,不要将一个整数或任何其他非地址类型的数据赋给一个指针变量。

*为指针运算符:功能是取其内部所存变量地址所指向变量中的内容。当指定j指向变量i之后,*j就完全等同于i,可以相互替换。使用时注意不要引用未初始化或null指针

定义指针变量时的*j和程序中用到的*j含义的不同。定义指针变量时的*j只是一个声明,此时的*仅表示该变量是一个指针变量,并没有其他含义。

4、指针变量的运算

两个指针变量相减的结果是这两个地址之间元素的个数,而不是地址的个数

所以只有同类型的指针能相减

5、指针与数组

数组名表示的是数组第一个元素的起始地址,
如果指针变量p已经指向一维数组的第一个元素,那么p+1就表示指向该数组的第二个元素
p指向的是第一个元素的地址,那么*p表示的就是第一个元素的内容。同样,p+i表示的是第i+1个元素的地址,那么*(p+i)就表示第i+1个元素的内容。即p+i就是指向元素a[i]的指针,*(p+i)就等价于a[i]。数组名a是一个常量,表示的是数组的首地址,那么元素a[i]的地址也可以用a+i表示。
a[i]写成*(a+i)的形式,程序的执行效率会更高、速度会更快。因为在执行程序的时候数组是先计算地址,然后转化成指针。而直接写成指针*(a+i)的形式就省去了这些重复计算地址的步骤。
指针还能用关系运算符比较大小,使用关系运算符来比较两个指针的值的前提是两个指针具有相同的类型

6、多级指针

int ****a; ”表示指针变量a只能存放int ***型变量的地址。
多级指针的目的是为了跨函数使用动态内存
“跨函数使用动态内存”也可以不用多级指针

7、函数指针

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址  

函数指针的定义方式:

函数返回值类型 (* 指针变量名) (函数参数列表);
int(*p)(int, int);

这个语句就定义了一个指向函数的指针变量p。首先它是一个指针变量,所以要有一个“*”,即(*p);
其次前面的int表示这个指针变量可以指向返回值类型为int型的函数;后面括号中的两个int表示这个指针变量可以指向有两个参数且都是int型的函数。函数指针的定义就是将“函数声明”中的“函数名”改成“(* 指针变量名)”。
这里需要注意的是:(* 指针变量名)两端的括号不能省略,
通过函数指针调用函数

# include <stdio.h>
int Max(int, int);  //函数声明
int main(void)
{
    int(*p)(int, int);  //定义一个函数指针
    int a, b, c;
    p = Max;  //把函数Max赋给指针变量p,使p指向Max函数
    printf("please enter a and b:");
    scanf("%d%d", &a, &b);
    c = (*p)(a, b);  //通过函数指针调用Max函数
    return 0;
}
int Max(int x, int y)  //定义Max函数
{
    int z;
    if (x > y)
    {
            z = x;
    }
    else
    {
            z = y;
    }
    return z;
}

三、常用库函数

1、sizeof

获得数据类型或变量在内存中所占的字节数,也可以获得整个数组在内存中所占的字节数

2、printf与scanf

输出控制符

  • %d:按十进制整型数据的实际长度输出。

        %ld:输出长整型数据。
        %md:m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际位数输出。

  • %u:输出无符号整型(unsigned)。输出无符号整型时也可以用%d,这时是将无符号转换成有符号数,然后输出。但编程的时候最好不要这么写,因为这样要进行一次转换,使CPU多做一次无用功。
  • %c:用来输出一个字符。
  • %f:用来输出实数,包括单精度和双精度,以小数形式输出。不指定字段宽度,由系统自动指定,整数部分全部输出,小数部分输出6位,超过6位的四舍五入。
  • %.mf:输出实数时小数点后保留m位,注意m前面有个点。
  • %o:以八进制整数形式输出,这个就用得很少了,了解一下就行了。
  • %s:用来输出字符串。用%s输出字符串同前面直接输出字符串是一样的。但是此时要先定义字符数组或字符指针存储或指向字符串,这个稍后再讲。
  • %x(或%X或%#x或%#X):以十六进制形式输出整数,小写的x,输出的字母就是小写的,大写的X输出的字母就是大写的;如果加一个“#”,就以标准的十六进制形式输出。最好是加一个“#”,否则如果输出的十六进制数正好没有字母的话会误认为是一个十进制数

转义符

输出“%d”只需在前面再加上一个“%”;要输出“\”只需在前面再加上一个“\”;要输出双引号也只需在前面加上一个“\”

scanf缓冲

scanf是缓冲输入的,也就是说从键盘输入的数据都会先存放在内存中的一个缓冲区。只有按回车键后scanf才会进入这个缓冲区和取数据,所取数据的个数取决于scanf中“输入参数”的个数。所以上述程序中scanf只有一个输入参数,因此按回车键后scanf只会取一个数据。所以变量ch有数据,而变量i没有数据,没有数据就是没有初始化,输出就是-858993460。

如果scanf中“输入参数”的个数为n,那么就从排在最前面的开始,依次往后取n个数据输出给scanf。没取完的仍旧放在缓冲区中,直到取用完毕为止。如果缓冲区中的数据全被取完了,但还有scanf要取数据,那就要再从键盘输入数据。
对于%d,在缓冲区中,空格、回车、Tab键都只是分隔符,不会被scanf当成数据取用。%d遇到它们就跳过,取下一个数据,这些被跳过去的空白符都被释放了。但是如果是%c,那么空格、回车、Tab键都会被当成数据输出给scanf取用,如果前面的有分隔符遗留在缓冲区可以用getchar()吸收,或者用fflush(stdin)清空缓冲区(并不是所有的编译器都支持fflush,比如gcc就不支持)

reinterpret_cast64<int>

指针类型强转

3.typeof()

通过typeof获取一个变量的类型int后,可以使用该类型再定义一个变量。

四、预处理指令

1、宏定义: #define

#define定义一个标识符来表示一个常量。其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。预编译又叫预处理。预编译不是编译,而是编译前的处理,预编译所执行的操作就是简单的“文本”替换。对宏定义而言,预编译的时候会将程序中所有出现“标识符”的地方全部用这个“常量”替换,称为“宏替换”或“宏展开”。替换完了之后再进行正式的编译

#define的作用域为自#define那一行起到源程序结束。如果要终止其作用域可以使用#undef命令

定义一个宏,求两个数的最大值。

 void)(&x==&y)的作用有两个:一是用来给用户提示一个警告,对于不同类型的指针比较,编译器会发出一个警告,提示两种数据的类型不同。二是两个数进行比较运算,运算的结果却没有用到,有些编译器可能会给出一个warning,加一个(void)后,就可以消除这个警告。

container_of:定义一个宏,根据结构体某一成员的地址,获取这个结构体的首地址。 

然后通过结构体的成员访问就可以访问其他成员变量了。这也是C语言的面向对象编程思想在Linux内核中的实现。

#define offset(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  //获取TYPE结构体的MEMBER成员偏移位置
#define container_of(ptr, type, member) ({  \
        const typeof(  (type *)0)->member  ) *__mprt = (ptr);  \  //使用临时指针变量__mptr来存储ptr的值时
        (type *)(   (char *)__mpter -  offset(type,member)    );})

2、# include 

include <stdio.h>也是这样的,即在预处理的时候先单纯地用头文件stdio.h中所有的“文本”内容替换程序中# include <stdio.h>这一行,然后再进行正式编译。

五、函数

C语言中,所有函数的定义,包括主函数main在内,都是“平行”的。也就是说,在一个函数的函数体内,不能再定义另一个函数,即不能嵌套定义

1)首先被调函数必须是已经存在的函数,要么是库函数,要么是自己定义的函数。如果是库函数,那么必须在程序开头用# include命令将该库函数所在的头文件包含进来。C语言中每一个库函数都有一个头文件,头文件包含了调用该库函数时所需要用到的信息,包括对库函数的函数声明。所以只要包含了某个库函数的头文件,就可以直接对该库函数进行调用,无需再进行声明

如果被调函数是用户自己定义的函数,而该函数的位置又在调用它的函数即主调函数的后面(在同一个文件中),那么就必须要在主调函数中,在调用位置之前对被调函数进行声明
如果被调函数的定义是在主调函数之前,那就可以不用对Max函数进行声明
在函数声明时也可以不写形参名,只写形参的类型。


六、递归

也是一种函数调用,只不过是函数自己调用自己,是一种特殊的函数调用
缺点也很明显:递归的优点是简化程序设计,结构简洁清晰,容易编程,可读性强,容易理解
速度慢,运行效率低,对存储空间的占用比循环多。
递归也带来了大量的函数调用,这也有许多额外的时间开销。函数调用要发送实参,要为被调函数分配存储空间,还要保存返回的值,又要释放空间并将值返回给主调函数,这些都太浪费空间和时间了!


七、变量的作用域、存储方式、修饰符

1、变量按作用域可分为

局部变量”:定义在函数内部的变量,只有在本函数内才能使用,函数调用完后,系统为该函数中的局部变量分配的内存空间就会被释放掉。在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,离开本复合语句就无效,且内存单元随即被释放。所谓复合语句就是用大括号“{}”括起来的多个语句。所以局部变量的作用范围准确地说不是以函数来限定的,而是以大括号“{}”来限定的。
全局变量”:定义在函数外部的变量,可以被整个C程序中所有的函数所共用。它的作用范围是从定义的位置开始一直到整个C程序结束。
全局变量未初始化,那么系统会自动将其初始化为0。它们的这个区别主要源自于它们存储空间的不同。局部变量是在栈中分配的,而全局变量是在静态存储区中分配的。只要是在静态存储区中分配的,如果未初始化则系统都会自动将其初始化为0。

外部变量(extern )”。
一般来说,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾结束,在一个文件内扩展全局变量的作用域,将外部变量的作用域扩展到其他文件

2、按存储方式可分为

“自动变量(auto)”、局部变量其实都是auto型
静态变量(static)”、statci修饰过的局部变量称为静态局部变量,定义成static之后就存储在静态存储区了。前面说过,存储在静态存储区中的变量如果未初始化,系统会自动将其初始化为0,静态存储区主要用于存放静态数据和全局数据。但它不是全局变量,
静态局部变量仍然是局部变量,仍然不能在它的作用范围之外使用。
静态局部变量仅在第一次函数调用时定义并初始化,以后再次调用时不再重新定义和初始化,而是保留上一次函数调用结束后的值。
用static修饰全局变量时,会限定全局变量的作用范围,使它的作用域仅限于本文件中。这个是使用static修饰全局变量的主要目的。如果不用static进行修饰,那么其他文件只需要用extern对该全局变量进行一下声明,就可以将该全局变量的作用范围扩展到该文件中。而且如果一个项目的多个.c文件中存在同名的全局变量,那么在编译的时候就会报错,报错的内容是“同一个变量被多次定义”。但是如果在这些全局变量前面都加上static,那么编译的时候就不会报错

3、const

const   int   a = 10;   与   int   const   a = 10;    两者语义一致
用const定义的变量的值是不允许改变的,即不允许给它重新赋值。所以说它定义的是只读变量必须在定义的时候就给它赋初值。如果定义的时候未初始化,对于未初始化的局部变量,程序在执行的时候会自动把一个很小的负数存放进去。

修饰谁,谁的内容就不可变,其他的都可变
当用const进行修饰时,根据const位置的不同有三种效果。原则是:修饰谁,谁的内容就不可变,其他的都可变

const int *p=&a;

当把const放最前面的时候,它修饰的就是*p,那么*p就不可变。*p表示的是指针变量p所指向的内存单元里面的内容,此时这个内容不可变。其他的都可变,如p中存放的是指向的内存单元的地址,这个地址可变,即p的指向可变。但指向谁,谁的内容就不可变,但它只能“禁止指针通过指针变量p修改”

int * const p=&a;

此时const修饰的是p,所以p中存放的内存单元的地址不可变,而内存单元中的内容可变。即p的指向不可变,p所指向的内存单元的内容可变

const int * const p=&a;

此时*p和p都被修饰了,那么p中存放的内存单元的地址和内存单元中的内容都不可变。

用const修饰的变量,无论是全局变量还是局部变量,生存周期都是程序运行的整个过程。而使用const修饰过的局部变量就有了静态特性,它的生存周期也是程序运行的整个过程。我们知道全局变量是静态的,静态的生存周期就是程序运行的整个过程。局部变量存储在栈中,静态变量存储在静态存储区中,而经过const修饰过的变量存储在内存中的“只读数据段”中。只读数据段中存放着常量和只读变量等不可修改的量。

八、结构体

struct student{
    char name[20];
    int age;
};

int main(void)
{
    sturct studet stu1 = {"aa",20};
    sturct studet stu2 = {
        .name = "aa",
        .age = 20
    };
}

九、__attribute__  属性声明

目前__attribute__支持十几种属性声明。● section.● aligned.● packed.● format.● weak.● alias.● noinline.● always_inline. 等

//后面接两对小括号
char c __attribute__((aligned(8))) = 4;  //按8字节对齐
__attribute__((packed,aligned(8))) char c1 = 4;
int a __attribute__((section(".data"))) ; //将函数或变量放到指定section

十、inline

inline是C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值