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()
{
PCHAR p1, p2;
char *p3, *p4;
}
2.void类型和void*类型的区别
#include <stdio.h>
void test01(void)
{
printf("%s", "hello world");
}
void test02()
{
}
int main()
{
test01();
}
3.struct成员不允许在定义时初始化
#include <stdio.h>
struct Person
{
char name[30] = {0};
int age = 20;
struct Person 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[])
{
return sizeof(arr);
}
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6};
printf("sizeof arr:%ld\n", sizeof arr);
printf("sizeof pointer:%d\n", calArraySize(arr));
}
5.指针步长的操作
不同类型的指针+1偏移的字节数是不一样的,这个和数据类型有关,int* 是4个字节,double* 是8个字节, 这个就叫做步长.
#include <stdio.h>
int main()
{
int * p;
printf("%p\n",p);
printf("%p\n",p+1);
}
思考:如何通过操作指针修改结构体中任意成员的值
struct Person
{
char a;
int b;
char c;
int d;
};
int main()
{
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;
};
int main()
{
struct Person p = {'a', 100, 'b', 200};
*(int *)((char *)&p + 12) = 1000;
printf("%d\n", p.d);
}
6.操作已回收的栈变量地址的问题
由于栈中的变量在出栈(离开作用域)后就会被回收,所有继续操作它是有问题的,例如:
#include <stdio.h>
int *myFun()
{
int a = 10;
return &a;
}
int main()
{
int *p = myFun();
return 0;
}
同样的再来看一个错误的例子:
#include <stdio.h>
char *getString()
{
char str[] = "hello world";
return str;
}
int main()
{
char *s = getString();
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);
memset(temp, 0, 100);
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);
memset(temp, 0, 100);
strcpy(temp, "hello world");
*c = temp;
}
int main()
{
char *p = NULL;
change(&p);
printf("p=%s\n", p);
free(p);
return 0;
}
此时p就能正常赋值了.
8.全局变量或者函数必须要先声明再使用
每一个.c文件就是一个编译单元,C语言编译器是单独编译每一个.c文件的,所以编译器在编译当前的.c文件的时候,如果发现存在没有声明的变量或者函数就会报错,虽然这个变量或者函数是全局的, 但是因为你存在其他的.c文件,所以编译器无法感知, 因此使用前必须先声明一下,可以在当前使用的.c文件中声明,用extern关键字, 也可以在.h文件中声明,然后通过include包含进来. 提示:声明外部的全局变量可以在函数内声明,也可以在函数外声明.
-
什么叫做变量的声明? 形如 extern int a; 这样的语句,也就是没有赋值的操作. -
什么叫做函数的声明? 形如 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);
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);
printf("%p\n", &a[0]);
printf("%p\n", (a + 1));
printf("%p\n", &a[1]);
int b[] = {1, 2, 3, 4};
printf("%p\n", b);
printf("%p\n", &b[0]);
printf("%p\n", (b + 1));
printf("%p\n", &b[1]);
}
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));
printf("b=%lu\n", offsetof(struct Person, b));
printf("c=%lu\n", offsetof(struct Person, c));
printf("d=%lu\n", offsetof(struct Person, d));
}
可以看到每个成员的首地址距离结构体的起始位置的偏移量都是不一样的. 那么如果我想获取到成员d的值就可以这样操作
int d = * (int *)((char*)&p + offsetof(struct Person,d));
printf("d=%d\n",d);
13.字符串拷贝的几种方式
这里介绍3种方式进行字符串的拷贝
#include <stdio.h>
void copy_string01(char *dest, char *source)
{
for (int i = 0; source[i] != '\0'; i++)
{
dest[i] = source[i];
}
}
void copy_string02(char *dest, char *source)
{
while (*source)
{
*dest = *source;
source++;
dest++;
}
}
void copy_string03(char *dest, char *source)
{
while (*dest++ == *source++);
}
int main()
{
char source[] = "hello world";
char dest[100] = {0};
copy_string01(dest, source);
printf("desc=%s\n", dest);
return 0;
}
|