C语言基础知识之define宏定义表达式,undef,内存对齐,a和&a的区别,数组知识点,int (*)[10] p,二维数组参数与二维指针参数,函数指针数组,常见的内存错误及对策

一.用define宏定义表达式

1.定义一年有多少秒:

#define SEC_A_YEAR 	60*60*24*365
//上述描述不可靠,没有考虑到在16位系统下把这样一个数赋给整型变量的时候可能会发生溢出
#define SEC_A_YEAR (60*60*24*365)UL

2.#undef

#undef是用来撤销宏定义的

#define PI 3.1415926
code...
#undef PI
//接下来的代码无法使用宏PI
//也就是说宏的生命周期从#define开始到#undef结束。

3.内存对齐

我们可以使用#pragma pack()来改变编译器的默认对齐方式。

使用指令#pragma pack(n),编译器将按照n个字节对齐。
使用指令#pragma pack(),编译器将取消自定义字节对齐方式。

虽然指定了按n字节对齐,但并不是所有的成员都是以n字节对齐。对齐的规则是每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是n个字节)中较小的一个对齐。

#pragma pack(8)
struct TestStruct1 {
	char a;
	long b;
}

struct TestStruct2 {
	char c;
	TestStruct1 d;
	long long e;
}
#pragma pack()
#include<stdio.h>

//#pragma pack(2)   //TestStruct1 6 TestStruct2 16
//#pragma pack(4)   //TestStruct1 8 TestStruct2 20
#pragma pack(8)     //TestStruct1 8 TestStruct2 24
//24的布局是当填充到e元素时前面已经使用了12个字节,但是后面的long long类型
//为8个字节,避免对这8个字节进行两次存取,所以需要填充4个字节,接着在放入long long类型。
//c         TestStruct1.a    TestStruct1.b  TestStruct2.e
//1***      1***                1111         **** 1111 1111
struct TestStruct1 {
    char a;
    long b;
};

struct TestStruct2 {
    char c;
    struct TestStruct1 d;
    long long e;
};

int main(){
    printf("sizeof(TestStruct1) = %d\n", sizeof(struct TestStruct1));
    printf("sizeof(TestStruct2) = %d\n", sizeof(struct TestStruct2));
}
#pragma pack()

总结一下:
a.每个成员分别按自己的方式对齐,并能最小化长度;
b.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度;
c.对齐后的长度必须是成员中最大的对其参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。

**二.int p = NULL 和 p = NULL有什么区别

//此时我们通过编译器查看p的值为0x00000000
int *p = NULL;
//下面这个可能就存在非法访问的问题了
//因为没有指定p的地址,出现野指针
int *p;
*p = NULL;

三.如何将数值存储到指定的内存地址

int *p = (int *)0x12ff7c;
*p = 0x100;
或者
*(int *)0x12ff7c = 0x100;

四.数组
1.a[5]与sizeof(a[5]):
sizeof(a[5])的值在32位系统下为4,并没有错,主要是因为sizeof是关键字不是函数,函数求值是在运行的时候,而关键字sizeof求值是在编译的时候。所以这里并没有真正去访问a[5].

2.&a[0]和&a的区别
a[0]是一个元素,a是整个数组,虽然&a[0]和&a的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。

3.数组名a作为左值和右值的区别

x=y
左值:在这个上下文环境中,编译器认为x的含义是x所代表的地址。
右值:在这个上下文环境中,编译器认为y的含义是y所代表的地址里面的内容。

当a作为右值的时候代表是数组的首地址吗,其实这并不是正确的,a作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组的首地址。仅仅只是代表并没有一个地方来存储这个地址,也就是说编译器并没有为数组a分配一块内存来存其地址。

a不能作为左值。

4.以指针的形式访问和以下标的形式访问指针

#include<stdio.h>

int main() {
    char *p = "abcdef";
    char a[] = "123456";

    //本质上以指针的形式访问和以下标的形式访问指针
    //是没有区别的,下标的访问也是先取出p里存储的地址值
    //然后再偏移4个元素去访问。
    printf("p[4]=%c\n",p[4]);
    printf("*(p+4)=%c\n",*(p+4));
    return 0;
}

5.a和&a的区别

#include<stdio.h>

int main() {
    int a[5] = {1,2,3,4,5};
    int *ptr = (int *)(&a+1);
    printf("%d,%d\n",*(a+1),*(ptr-1)); //2,5

	//p3和p4都是数组指针,指向的是整个数组,&a是整个数组的首地址
	//a是数组首元素的首地址。其中p3两边的数据类型完全一致的。而p4
	//两边的数据类型就不一致的,左边的类型是指向整个数组的指针,右边
	//的数据类型是指向单个字符的指针。所以一般编译器会报警告的。
	char a[5] = {'A','B','C','D'};
	char (*p1)[5] = &a;
	char (*p2)[5] = a;

	printf("%s,%s\n",p1+1,p2+1);

    char (*p3)[3] = &a;
    char (*p4)[3] = a;

    printf("%s,%s\n",p3+1,p4+1);

    char (*p5)[10] = &a;
    char (*p6)[10] = a;

    printf("%s,%s\n",p5+1,p6+1);
	//结语:由于&a和a的值是一样的,而变量作为右值时编译器只是取变量的值
	//所以运行并没有什么问题。不过最好不要这么使用。
    return 0;
}

6.int (*)[10] p
int (*)[10]p 其实和int (*p)[10]一样都是定义数组指针的。其实数组指针的原型就是这样的。

7.地址的强制转换

#include<stdio.h>

int main() {
    struct Test{
        int Num;
        char *pcName;
        short sDate;
        char cha[2];
        short sBa[4];
    }*p;

    printf("p = %p\n", p);
    printf("sizeof(*p) = %d\n", sizeof(*p));

    //指针变量与一个整数相加减并不是用指针变量里的地址
    //直接加减这个整数。这个整数的单位不是byte而是元素的个数
    printf("p+0x1 = %p\n", p+0x1);
    printf("(unsigned long)p+0x1 = %p\n", (unsigned long)p+0x1);
    printf("(unsigned int *)p+0x1 = %p\n", (unsigned int*)p+0x1);
    return 0;
}

在这里插入图片描述

五.无法向函数传递一个数组

#include<stdio.h>

//C语言中,当一维数组作为函数参数的时候,编译器总是
//把它解析成一个指向其首元素首地址的指针。
//void fun(char  a[10]) {
//void fun(char *a) {
void fun(char a[]) {
    char c = a[3];

    printf("sizeof(a) = %d\n", sizeof(a));
    printf("c = %c\n", c);
}

int main() {
    char b[10] = "abcdefg";
    fun(b);
    return 0;
}

六.无法把指针变量本身传递给一个函数

#include<stdio.h>

void fun(char *p) {
    char c = p[3];
    p[2] = 'f';

    printf("p = %p\n", p);
    printf("&p = %p\n", &p);
    printf("p = %s\n", p);
    printf("sizeof(a) = %d\n", sizeof(p));
    printf("c = %c\n", c);
}

int main() {
    char b[10] = "abcdefg";
    fun(b);
    printf("b = %p\n", b);    printf("&b = %p\n", &b);
    printf("b = %s\n", b);
    return 0;
}

在这里插入图片描述

因为无法把指针变量本身传递给一个函数
应该对实参做一份拷贝并传递给被调用函数,即对b做一份拷贝,假设其拷贝名为_b,那传递到函数内部的就是_b,而并非b本身。

七.无法把指针变量本身传递给一个函数

#include<stdio.h>

void GetMemory(char *p, int num)
{
    p = (char *)malloc(num*sizeof(char));
}
int main()
{
    char *str = NULL;
    GetMemory(str, 10);

    printf("str = %p\n", str);
    strcpy(str,"hello");
    free(str); //free 并没有起作用,内存泄漏
    return 0;
}

在这里插入图片描述
在运行strcpy(str, “hello”)语句的时候会发生错误,这时候str的值仍然为NULL,我们malloc的内存的地址并没有赋给str,
而是赋给了_str.而这个_str是编译器自动分配和回收的,我们根本就无法使用。

方法一:利用return

#include<stdio.h>

char * GetMemory(char *p, int num)
{
    p = (char *)malloc(num*sizeof(char));
    return p;
}
int main()
{
    char *str = NULL;
    str = GetMemory(str, 10);

    printf("str = %p\n", str);
    strcpy(str,"hello");
    free(str); //free 并没有起作用,内存泄漏
    return 0;
}

方法二:利用二级指针

#include<stdio.h>

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num*sizeof(char));
}

int main()
{
    char *str = NULL;
    GetMemory(&str, 10);

    printf("str = %p\n", str);
    strcpy(str,"hello");
    free(str); //free 并没有起作用,内存泄漏
    return 0;
}

八.二维数组参数与二维指针参数

void fun(char a[3][4]);
void fun(char a[][4]);

我们完全可以把a[3][4]理解为一个一维数组a[3],其每个元素都是一个含有4个char类型数据的数组。
C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。
也就是说我们可以把函数声明改写成:
void fun(char (*p)[4])

二维数组参数和二维指针参数的等效关系
在这里插入图片描述
C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维
再也不可改写。比如: a[3][4][5]作为参数时可以被改写为(*p) [4][5]

九.函数指针数组

#include<stdio.h>

char *fun1(char *p) {
    printf("[%s:%d] %s\n",__func__, __LINE__, p);
    return p;
}

char *fun2(char *p) {
    printf("[%s:%d] %s\n",__func__, __LINE__, p);
    return p;
}

char *fun3(char *p) {
    printf("[%s:%d] %s\n",__func__, __LINE__, p);
    return p;
}

int main()
{
    char *(*pf[3])(char *p);
    pf[0] = fun1;//可以直接用函数名
    pf[1] = &fun2;//可以用函数名加上取地址符
    pf[2] = &fun3;

    pf[0]("fun1");
    pf[1]("fun2");
    pf[2]("fun3");
    return 0;
}

十.避免野指针
方法就是在定义指针变量的时候同时最好初始化为NULL。用完指针之后也将指针变量的值设置为NULL.

十一.常见的内存错误及对策
1.指针没有指向一块合法的内存
1.1 结构体成员指针未初始化

#include<stdio.h>

struct student {
    char *name;
    int score;
}stu,*pstu;

int main()
{
    strcpy(stu.name, "Jimy");
    stu.score = 99;
    printf("stu.score = %d\n", stu.score);
    printf("stu.name = %s\n", stu.name);
    return 0;
}

这里定义了结构体变量stu,但是没想到这个结构体内部char*name这成员在定义结构体变量stu时。只是给name这个指针变量本身分配了4个字节。name指针并没有指向一个合法的地址,所以在调用strcpy函数时,会将字符串Jimy往这个非法的内存拷贝,所以会导致出错。解决的办法就是为name指针malloc一块空间。

#include<stdio.h>

struct student {
    char *name;
    int score;
}stu,*pstu;

int main()
{
    pstu = (struct student *)malloc(sizeof(struct student));
    strcpy(pstu->name, "Jimy");
    pstu->score = 99;
    printf("pstu->score = %d\n", pstu->score);
    printf("pstu->name = %s\n", pstu->name);
    free(pstu);
    return 0;
}

上面同样也是没有给name指针分配内存。

1.2没有为结构体指针分配足够的内存

#include<stdio.h>

struct student {
    char *name;
    int score;
}stu,*pstu;

int main()
{
    //这里误把sizeof(struct student)写成sizeof(struct student *)
    pstu = (struct student *)malloc(sizeof(struct student *));//4
    pstu->name = (char *)malloc(32);
    strcpy(pstu->name, "Jimy");
    pstu->score = 99;
    printf("sizeof(struct student *)= %d\n", sizeof(struct student *));
    printf("pstu->score = %d\n", pstu->score);
    printf("pstu->name = %s\n", pstu->name);
    free(pstu->name);
    free(pstu);
    return 0;
}

1.3函数的入口校验
不管什么时候,我们使用指针之前一定要确保指针是有效的。

2.为指针分配的内存太小
为指针分配了内存,但是内存大小不够,导致出现越界错误。

char *p1 = "abcdefg";
char *p2 = (char *)malloc(sizeof(char)*strlen(p1));
strcpy(p2,p1);
//p1是字符串变量,其长度为7个字符,但其所占内存大小为8个byte。
//初学者往往忘记了字符串常量的结束标志\0
char *p2 = (char *)malloc(sizeof(char)*strlen(p1) + 1*sizeof(char));
//只有字符串常量才有结束标志符的,下面的就没有的
char a[7] = {'A','B','C','D','E'};

3.内存分配成功,但并未初始化

所以在定义一个变量时,第一件事就是初始化,你可以把它初始化未一个有效的值。

int i = 10;
char *p = (char *)malloc(sizeof(char));
但是往往这个时候我们还不确定这个变量的初值,这样的话可以初始化为0或NULL.
int i = 0;
char * p = NULL;
如果定义的是数组的话,可以这样初始化:
int a[10] = {0};
或者利用memset函数来初始化为0
memset(a, 0 , sizeof(a));

十二.如何使用malloc函数

1.使用malloc函数申请堆区空间的时候,内存分配失败会失败,函数会返回NULL.
既然malloc函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用if(NULL != p)
语句来验证内存确实分配成功了的。

2.用malloc函数申请0字节内存

#include<stdio.h>

int main()
{
    char *ch = (char *)malloc(0);
    *ch = 'a';
    printf("ch = %p\n", ch);

    return 0;
}

注意申请0字节的内存,函数并不放回NULL,这是if(NULL != p)语句校验将会失效的。

3.内存释放之后
使用free函数之后指针变量p本身保存的地址并没有改变,那我们就需要重新把p的值变为NULL;
p = NULL;

十三.return语句不可返回指向"栈内存"的指针,因为该内存在函数体结束时被自动销毁,
如:

#include<stdio.h>

char *Func(void) {
    char str[30] = "tomtom";
    return str;
}

int main()
{
    char *p = Func();
    printf("Func return value = %s\n", Func());
    printf("p = %s\n", p);

    return 0;
}

在这里插入图片描述
str属于局部变量,位于栈内存中,在Func结束的时候被释放,所以返回str将导致错误。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值