C基础补充

Posted mChenys

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C基础补充相关的知识,希望对你有一定的参考价值。

目录

1.结构体使用typedef起别名的2种方式

typedef起别名的作用类似kotlin的typealias, 由于结构体也是数据类型的一种,因此也可以起别名

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct Person

    char name[64];
    int age;
;
//方式一:
typedef struct Person MyPerson;

//方式二:在声明结构体的时候就定义别名
typedef struct Student

    char name[64];
    int age;
 MyStudent;

int main()

    MyPerson p = "张三", 20;
    printf("p.name=%s,p.age=%d\\n", p.name, p.age);

    MyStudent s = "小明", 30;
    printf("s.name=%s,age=%d\\n", s.name, s.age);

    return 0;


思考 char* p1,p2中p1和p2是同一类型吗?
其实,它们的类型是不一样的,p1是char *类型,而p2是char类型.
如果要变成同一种类型,可以这样干

typedef char *PCHAR;
int main()

    // 使用typedef定义的类型
    PCHAR p1, p2;
    
    // 或者这样也是定义2个char*类型
    char *p3, *p4;

2.void类型和void*类型的区别

#include <stdio.h>

// 1.void 修饰函数参数和函数返回
void test01(void)

    printf("%s", "hello world");


// 2.不能定义void类型变量
void test02()

    // void va1;//报错


int main()

    test01(); // hello world


3.struct成员不允许在定义时初始化

#include <stdio.h>

struct Person

    char name[30] = 0; // 编译报错
    int age = 20; // 编译报错
    struct Person p; //编译报错,因为编译器不知道p要分配多少内存
    //编译报错,成员的初始化必须要等结构体本身初始化之后才行
    struct Person p = "hello",20;
;

4.sizeof操作符的注意事项

sizeof是c语言中的一个操作符,类似++、–等等,sizeof能够告诉我们编译器为某一特定数据或者某一类型的数据在内存中分配空间的大小,大小以字节位单位.
基本语法:

sizeof(变量);
sizeof 变量;
sizeof(类型);

注意:

  • sizeof返回的占用空间大小是为这个变量开辟的大小,而不是它用到的空间大小.
  • sizeof的返回值类型是unsigned int; (无符号的数去减一个数结果还可能是无符号的,所以sizeof的返回值不要拿去做运算).
  • 要注意数组名和指针变量的区别,对数组名使用sizeof返回的是整个数组的大小,而对指针变量使用sizeof返回的是指针类型变量所占的大小,这个和操作系统的位数有关,32位占4个字节,64位占8个字节.

C语言规定当一个数组名作为函数的形参的时候,它就变成了指向数组首元素地址的指针变量了.
所以sizeof返回值就不在是数组的大小了,例如:

#include <stdio.h>

int calArraySize(int arr[]) //其实和int *arr没有区别了.

    return sizeof(arr); //这里其实已经把arr单做指针变量计算了


int main()

    int arr[] = 1, 2, 3, 4, 5, 6;
    printf("sizeof arr:%ld\\n", sizeof arr);           //24, 数组大小等于元素个数*元素类型长度
    printf("sizeof pointer:%d\\n", calArraySize(arr)); //8,数组作为函数参数会退化为指向数组首元素的指针

5.指针步长的操作

不同类型的指针+1偏移的字节数是不一样的,这个和数据类型有关,int*是4个字节,double*是8个字节, 这个就叫做步长.

#include <stdio.h>

int main()

    int * p;
    printf("%p\\n",p); // 0x0
    printf("%p\\n",p+1); // 0x4

思考:如何通过操作指针修改结构体中任意成员的值

struct Person

    char a;
    int b;
    char c;
    int d; //如何通过指针操作修改成员d的值
;
int main()

    // 思考: 如何通过指针操作修改p结构体中d成员的值?
    struct Person p = 'a', 100, 'b', 200;

我们都知道结构体是一种数据类型,它的大小是由里面的成员个数和种类决定的,所以结构体的指针的步长和结构体的大小有关,结构体类型的指针偏移一个单位,大小就是偏移一个结构体大小.

那么如果修改步长是不是就可以操作任意成员呢?
对的,我们可以通过强转指针类型为char*类型,这样步长就是1个字节了.根据结构体的内存对齐模式可知偏移12个字节就可以定位到成员d的首地址了,然后再强转成int*类型就可以得到d成员的指针类型了,然后就可以取*操作值了.
答案就是:

#include <stdio.h>

struct Person

    char a;
    int b;
    char c;
    int d; //如何通过指针操作修改成员d的值
;
int main()

    // 思考: 如何通过指针操作修改p结构体中d成员的值?
    struct Person p = 'a', 100, 'b', 200;

    //对p先取地址得到指针类型,然后强转char*类型,然后偏移12个字节,然后再强转成int*类型,然后再取*操作d成员的值
    *(int *)((char *)&p + 12) = 1000;
    printf("%d\\n", p.d); // 1000

6.操作已回收的栈变量地址的问题

由于栈中的变量在出栈(离开作用域)后就会被回收,所有继续操作它是有问题的,例如:

#include <stdio.h>

int *myFun()

    int a = 10;
    return &a; // 编译器会有警告


int main()

    int *p = myFun(); //这里去接收a变量的地址是有问题的,因为a离开函数作用域就死掉了.
    return 0;

同样的再来看一个错误的例子:

#include <stdio.h>

char *getString()

    char str[] = "hello world";
    return str;


int main()

    char *s = getString(); //这里也是有问题的,因为str离开函数体后也死掉了,所以它指向的地址也是有问题的
    printf("s=%s\\n", s);
    return 0;


可以看到上面2个都在编译时就提示了警告了, 如何解决呢?

很简单,只需要将栈变量指向的地址改成堆中分配的地址就可以了,这样当函数结束的时候虽然栈中的变量死了,但是它指向的堆的空间还是有效的,因为堆内存需要程序员手动释放才被回收,所以返回堆的地址是还可以继续使用.

7.形参不能修改实参的问题

由于函数的形参在函数体内怎么变化,它都无法影响到实参,所以想修改实参的数据,必须要传递实参的地址,否则会有问题,例如下面这段代码就是有问题的.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void change(char *c)

    char *temp = malloc(100);//分配100个字节的堆内存
    memset(temp, 0, 100); //重置为0
    strcpy(temp, "hello world");
    c = temp;


int main()

    char *p = NULL;
    change(p);
    printf("p=%s\\n", p);
    free(p);
    return 0;

结果是

发现p指针赋值失败了,这是因为p是实参,而change函数内部改变的是形参c的值,而且这里还存在内存泄露的问题,因为形参c离开函数后就挂了,然后它之前在堆中申请的100个字节的空间还没有释放,而且没有任何指针指向这块区域.

解决方案如下:
将函数形参变成二级指针,实参p改为传递地址就OK了.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void change(char **c)

    char *temp = malloc(100); //分配100个字节的堆内存
    memset(temp, 0, 100);     //重置为0
    strcpy(temp, "hello world");
    *c = temp; //取1级指针后赋值为temp指向的堆空间地址


int main()

    char *p = NULL;
    change(&p); //这里传递指针p的地址
    printf("p=%s\\n", p);
    free(p);
    return 0;

此时p就能正常赋值了.

8.全局变量或者函数必须要先声明再使用

每一个.c文件就是一个编译单元,C语言编译器是单独编译每一个.c文件的,所以编译器在编译当前的.c文件的时候,如果发现存在没有声明的变量或者函数就会报错,虽然这个变量或者函数是全局的, 但是因为你存在其他的.c文件,所以编译器无法感知, 因此使用前必须先声明一下,可以在当前使用的.c文件中声明,用extern关键字, 也可以在.h文件中声明,然后通过include包含进来.
提示:声明外部的全局变量可以在函数内声明,也可以在函数外声明.

  1. 什么叫做变量的声明?
    形如 extern int a; 这样的语句,也就是没有赋值的操作.

  2. 什么叫做函数的声明?
    形如 extern int add(int a, int b);这样的语句,也就是没有函数体的函数.

例如在test2.c文件内定义如下:

 int a = 100; 

 void changeA()
     a = 200;
 

然后在test.c内通过extern来使用test2的变量和函数

#include <stdio.h>

// 声明外部全局变量
extern int a;
// 声明外部全局函数
extern void changeA();

int main()
   
    // 访问外部变量
    printf("before change a is %d\\n", a);
    // 调用外部函数
    changeA();
    // 访问外部变量
    printf("after change  a is %d\\n", a);
    return 0;

现在假设直接编译test.c的话会看到如下错误:

正确的编译姿势是如下:

需要将定义全局变量和全局函数的test2.c文件和test.c一起编译.

9.const全局变量和const局部变量的区别

1)如果const修饰的是全局变量,那么它是存在常量区,和字符串常量存放的地方一样,全局唯一, 是不能修改的.
2)如果const修饰的是局部变量,那么它在栈区,是可以被间接修改的.

不同的编译器对字符串常量的处理也不一样,有些编译器是可以通过指针更改字符串常量的值的, 这种编译器通常是相同的字符串存在2个不同的地址.而大多数编译器都会对字符串常量进行优化,相同的字符串只会存在一个,那么它们的地址就是一样的.这种编译器是不允许修改字符串常量的值的.

#include <stdio.h>

int main()

    char *p1 = "hello";
    char *p2 = "hello";
    printf("p1=%p\\np2=%p\\n", p1, p2); //这里输出的值是一样的."hello"字符串在常量区只存在一份
    return 0;

结果如下

10.宏函数与普通函数的区别

宏函数并不是真正的函数,但是在一些场景中它的效率要高于普通的函数,这是由于宏函数没有普通函数参数压栈/跳转/返回的开销, 可以提高程序效率.

宏定义只在定义的文件中起作用,无参数的宏定义称为宏常量,带参数的宏定义称为宏函数

注意:
宏名一般大写,以便和变量区别;
宏定义可以是常量,表达式等;
宏定义不是C语言,不需要在行末加分号
宏名有效范围从定义到本源文件结束
可以使用#undef命令终止宏定义的作用域
在宏定义,可以引用已定义的宏名
对应宏函数的参数,需要用括号括住每一个参数,并括住宏的整体定义,避免宏函数和其他数值进行运算时宏展开得到的运算结果不符合预期

#include <stdio.h>

// 定义宏函数
#define MYADD(x, y) ((x) + (y))
// 定义宏常量
#define MAX 1024

// 普通函数
int add(int a, int b)

    return a + b;


int main()

    int a = 10;
    int b = 20;
    printf("a+b=%d\\n", MYADD(a, b));
    return 0;

11.正确认识字符串及数组的首地址

注意:字符串常量名以及数组名指向的是首元素的地址而不是末尾元素的地址.

#include <stdio.h>

int main()

    char *a = "abcd";
    printf("%p\\n", a);       // 指针变量名指向的是首元素的地址,0x104623fac
    printf("%p\\n", &a[0]);   // 字符串第一个元素的地址:0x104623fac
    printf("%p\\n", (a + 1)); // 指针+1后,对应的地址:0x104623fad
    printf("%p\\n", &a[1]);   // 字符串第二个元素的地址:0x104623fad

    int b[] = 1, 2, 3, 4;
    printf("%p\\n", b);       //数组名指向首元素的地址,0x16f6930a0
    printf("%p\\n", &b[0]);   // 数组首元素的地址:0x16f6930a0
    printf("%p\\n", (b + 1)); // 数组名+1也就是指针+1,对应地址:0x16f6930a4
    printf("%p\\n", &b[1]);   //数组第二个元素的地址:0x16f6930a4

12.获取结构体成员的地址偏移量

由于结构体有内存对齐模式,所以自己算比较麻烦,我们可以借助函数来实现
使用offsetof函数, 需要引入stddef.h库

size_t offsetof(type, member);

参数1是数据类型
参数2是要查找的结构体成员的变量名
返回值是该成员的首地址距离结构体的起始地址的偏移量,单位字节

用法如下:

#include <stdio.h>
#include <stddef.h>
struct Person

    int a;
    char b;
    char c[64];
    int d;
;

int main()

    struct Person p = 10, 'a', "hello world", 100;

    printf("a=%lu\\n", offsetof(struct Person, a)); // a=0
    printf("b=%lu\\n", offsetof(struct Person, b)); // b=4
    printf("c=%lu\\n", offsetof(struct Person, c)); // c=5
    printf("d=%lu\\n", offsetof(struct Person, d)); // d=72

可以看到每个成员的首地址距离结构体的起始位置的偏移量都是不一样的.
那么如果我想获取到成员d的值就可以这样操作

//1.首先对p取地址并强转成char*,那么指针的步长就是1了,
//2.然后加上d成员的偏移量,这样就取到了d成员的首地址
//3.然后再强转成int*,那么就得到了d成员的首地址
//4.然后再取*就可以取出这个地址对应的值了,也就是d的值, 100
int d = * (int *)((char*)&p + offsetof(struct Person,d));
printf("d=%d\\n",d);//100

13.字符串拷贝的几种方式

这里介绍3种方式进行字符串的拷贝

#include <stdio.h>

//方式1
void copy_string01(char *dest, char *source)

    for (int i = 0; source[i] != '\\0'; i++)
    
        dest[i] = source[i]; //[]方式赋值
    


//方式2
void copy_string02(char *dest, char *source)

    while (*source)
    
        *dest = *source; //指针取*方式赋值
        source++;
        dest++;
    


//方式3
void copy_string03(char *dest, char *source)

    while (*dest++ == *source++); //指针取*方式赋值


int main()

    char source[] = "hello world";
    char dest[100] = 0;
    copy_string01(dest, source);
    //copy_string02(dest, source);
    //copy_string03(dest, source);

    printf("desc=%s\\n", dest);
    return 0;

14.数组下标能否是负数

可以,把数组当做指针看待就可以了,例如p[-1], 其实编译器会理解成*(p-1);例如:

#include <stdio.h>
int main()

    int arr[3] = 1, 2, 3;
    int *p = arr;
    p += 1;            //移动一个步长单位
    int b = p[-1];     //编译器会理解成 *(p-1);
    printf("%d\\n", b); //结果是1
    return 0;

15.选择排序

15.1 数组排序

选择排序的思想就是通过内循环查找最小值的下标,然后外循环进行交换位置(与约定的最小值进行交换)

#include <stdio.h>

void print_array(int *arr, int len)

    for (int i = 0; i < len以上是关于C基础补充的主要内容,如果未能解决你的问题,请参考以下文章

c语言设计模式--补充面向对象基础

C基础补充

C基础补充

python介绍和基础(待补充)

python基础补充

4Go语言基础之输出方式知识点补充