C深剖关键字(3)

简介: 目录一:void关键字二:return关键字三:const关键字

一:void关键字

问题:void能否定义变量?




从图中不难发现,void定义x出错,可原因是什么呢?


定义变量的本质:开辟空间


而void作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待


所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量。


void的大小在不同编译器下是不一样的


在vs2013中,sizeof(void)=0


在Linux中,sizeof(void)=1(但编译器依旧理解成,无法定义变量)


void本身就被编译器解释为空类型,强制的不允许定义变量。


void修饰函数返回值:


看代码:

#include<stdio.h>
void test()
{
  printf("hello test\n");
  return 1;
}
int main()
{
  test();
  return 0;
}

但是当我们去掉void返回值的时候再试试:

#include<stdio.h>
test()
{
  printf("hello test\n");
  return 1;
}
int main()
{
  int a = test();     // hello test
  printf("%d\n", a);  // 1
  return 0;
}

我们发现编译器可以正常运行没有报错,由此得到以下结论:


//如果自定义函数,或者库函数不需要返回值,那么就可以写成void


//那么问题来了,可以不写吗?不可以,自定义函数的默认返回值是int(这个上述验证)


//所以,没有返回值,如果不写void,会让阅读你代码的人产生误解:他是忘了写,还是想默认int?


//结论:void作为函数返回值,代表不需要,这里是一个"占位符"的概念,是告知编译器和给阅读源代码的工程师看的。


void修饰函数参数:


看代码:

#include <stdio.h>
int test1() //函数默认不需要参数
{
  return 1;
}
int test2(void) //明确函数不需要参数
{
  return 1;
}
int main()
{
  //test1的形参列表为空,可不可以给它传参呢?
  test1(1, 2, 3, 4); //依旧传入参数,编译器不会告警或者报错
  test2(1, 2, 3, 4); //依旧传入参数,编译器会告警(vs)或者报错(gcc)
  return 0;
}

总结:


结论:如果一个函数没有参数,将参数列表设置成void,是一个不错的习惯,因为可以将错误明确提前发现


另外,阅读你代码的人,也一眼看出,不需要参数。相当于"自解释"。


void充当函数的形参列表,告知用户or编译器,该函数不需要传参


void指针:


void不能定义变量,那么void*呢?

#include<stdio.h>
int main()
{
  void* p = NULL;
  return 0;
}

这里编译器并没有报错,为什么void*可以呢?

因为void*是指针,是指针,空间大小就能明确出来

只要是指针,在32位就占4个字节,在64位就占8个字节。

void* 能够接受任意指针类型:&&  void*可以接受任意指针类型:

#include<stdio.h>
int main()
{
  void* p = NULL;
  double* x = NULL;
  int* y = NULL;
  x = p;
  y = p;
  return 0;
}

此代码块运行编译器没有报错。

x=p和y=p相当于把p指针变量的值赋给x和y,而x的类型是double*,y的类型是int*p的类型是void*,等号左右两边类型不一样。

结论:void*可以被任何指针类型接受

当我们把x=p和y=p调换位置呢。

#include<stdio.h>
int main()
{
  void* p = NULL;
  double* x = NULL;
  int* y = NULL;
  p = x;
  p = y;
  return 0;
}

编译器依旧没有报错。

结论:void*可以接受任意指针类型(常用)

void * 定义的指针变量可以进行运算操作吗?

#include<stdio.h>
int main()
{
  //void* p = NULL;
  int* p = NULL;
  p++;
  p--;
  // 对于整型指针是没有问题的
  //而void*是空类型,在vs大小为0,而使用p++或--需要明确大小,故不行
  return 0;
}

// 对于整型指针是没有问题的

//而void*是空类型,在vs大小为0,而使用p++或--需要明确大小,故不行

void*可以直接解引用吗?

#include<stdio.h>
int main()
{
  //  void* p = NULL;
  //  *p; // err
  int a = 10;
  void* p = (void*)&a;
  // *p; // err
  *p = 20;  // err
  return 0;
}

原因:

p是void*指针类型,那么解引用*p它指向的目标就是void,而其空间大小无法得知

结论:void*不能直接解引用

二:return关键字

看代码:

#include<stdio.h>
char* show()
{
    char str[] = "helloworld";
  return str;
}
int main()
{
  char* s = show();
  printf("%s\n", s);
  return 0;
}

image.png

运行起来确是一串乱码 ,为什么呢?


此时在函数内定义的char str[] = "hello bit";数组是在栈上开辟的空间,它本质上是一份临时空间,函数调用的时候就在栈上开辟空间,函数调用完毕,栈结构,临时变量,数据就会被释放掉,所以打印乱码。


C语言有字符串但没有字符串类型


计算机如何删除数据呢?


//计算机中,释放空间是否真的要将我们的数据全部清0/1 ?


//计算机中清空数据,只要设置该数据无效即可


重新分析下上述的代码:


show函数是函数调用,调用时就要开辟空间,在这块空间中要开辟一块数组空间,而这块空间是在哪开辟的呢?接下来需要回顾下曾经讲过的内容。地址空间划分有如下几块:


image.png

image.png

调用函数,形成栈帧,函数返回,释放栈帧,但是数据并没有被清空,仅仅表明这块空间是可被覆盖的,printf也是函数,调用printf形成栈帧,返回printf释放栈帧,形成新的栈帧就要覆盖原先老的栈帧,所以helloworld字符串便不存在


//为什么临时变量具有临时性!


//栈帧结构在函数调用完毕,需要被释放

#include<stdio.h>
int GetData()
{
    int x = 0x11223344;
    printf("run get data!\n");
    return x;
}
int main()
{
    int y = GetData();
    printf("ret:%x\n", y);
    return 0;
}

image.png既然x是函数定义的变量,具有临时性,那么这个临时变量在函数退出的时候,应该被释放,但为什么还能正常打印呢?


通过查看汇编可以看到,对于一般内置类型,寄存器eax可以充当返回值的临时空间


函数的返回值,通过寄存器的方式,返回给函数调用方!


三:const关键字

const修饰的变量是不可直接被修改!!!

#include<stdio.h>
int main()
{
  const int a = 10;
  // 等价于: int const a = 10;
  a = 20;  // err
  return 0;
}

但是可以通过指针的方式间接修改!!!

image.png


那const修饰变量,意义何在?


1. 让编译器进行直接修改式检查


2. 告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改哦。也属于一种“自描述”含义


const修饰的变量,可以作为数组定义的一部分吗?


#include<stdio.h>
int main()
{
  const int n = 100;
  int arr[n];  // err 编译器报错
  return 0;
}

//在vs2013(标准C)下直接报错了,但是在gcc(GNU扩展)下,可以,主要是不同编译器对C语言的标准不同。

//但我们一切向标准看齐,不可以。

const修饰数组:

#include<stdio.h>
int main()
{
  const int arr[] = { 1,2,3,4,5 };
  arr[0] = 0;   //err
    arr[1] = 0;   //err
  arr[2] = 0;   //err
  arr[3] = 0;   //err
  return 0;
}

编译器报错,const修饰数组的话,代表的是数组的每一个元素都必须是不可被修改的,或者是只读数组

const修饰指针:

看代码:先总体了解下:

#include<stdio.h>
int main()
{
  int a = 10;
  int* p = &a;
  *p = 100;  // *p=100对p进行解引用,代表的是p所指向的目标,所以*p就是a,换句话说是把a的值改为100
  p = 100;   // p=100,p本身也是一个变量,本来p指向a变量的,现在p指向地址为100的一个变量
  return 0;
}

// *p=100对p进行解引用,代表的是p所指向的目标,所以*p就是a,换句话说是把a的值改为100

// p=100,p本身也是一个变量,本来p指向a变量的,现在p指向地址为100的一个变量

情形一:

#include<stdio.h>
int main()
{
  int a = 10;
  const int* p = &a;    //p指向的变量不可直接被修改
  *p = 100;  // err    
  p = 100;   
  return 0;
}

此时,*p=100;编译器报错,理由如下:


// 此时的const修饰的是*p,不是p,也就是说不是p无法改变,而是p指向的内容无法改变


// 此时如果对p进行解引用,那么这个值是不能被修改的,换言之p指向的变量不可被修改,p无法被解引用后充当左值


// 所以p=100不会报错


情形二:

{
  int a = 10;
  int const * p = &a; //p指向的变量不可直接被修改
  *p = 100;  // err 
  p = 100;   
  return 0;
}

此情况和上述第一种一样,const修饰的都是*p而不是p,只不过将const挪位到int的右边,但不影响结果,理由和情形一相同。

情形三:

#include<stdio.h>
int main()
{
  int a = 10;
  int* const p = &a;  //p的内容不可直接被修改,p指向不能改
  *p = 100;  
  p = 100;   // err
  return 0;
}

int* const p = &a;  //此时的const在*的右侧,在p的左侧,则这个const修饰的是p变量,这样写和const int a = 10;没有差别,则就表明p的值不可直接被修改,p指向不能改


*p = 100;  // 但是可以间接修改,此时*p指向a,a可以被修改,*p=100;是没有问题的


而p=100;就是错的。


情形四:

#include<stdio.h>
int main()
{
  int a = 10;
  const int* const p = &a; 
  *p = 100;  // err
  p = 100;   // err
  return 0;
}

const int* const p = &a; //最左边的const指向*,代表p指向的不可以直接被修改,而离p最近的const修饰的是p,代表的是p的指向不能被修改

所以*p=100和p=100都是错的。

const修饰函数参数:

#include<stdio.h>
void show(const int* _p)
{
  printf("value:%d\n", _p);
}
int main()
{
  int a = 10;
  int* p = &a;
  show(p);
  return 0;
}

一般我们在设计的时候,因为show函数从设计角度本质就是打印,内部的打印就不会对传的参数进行修改,为了写出严谨的代码,应该在传参的时候带上const,代表的是此时_p指针变量所指向的内容不可改,也就是对应的a是不可改的

const修饰函数返回值:

代码解释:

#include<stdio.h>
//告诉编译器,告诉函数调用者,不要试图通过指针修改返回值指向的内容
const int* GetVal()
{
  static int a = 10;
  return &a;
}
int main()
{
    // int *p = test();   //有告警
  const int* p = GetVal();  //需要用const int*类型接受
    *p=200; //err  //这样,【在语法/语义上】,限制了,不能直接修改函数的返回值
  return 0;
}
//一般内置类型返回,加const无意义。


相关文章
|
5天前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
32 5
|
2月前
|
编译器 C语言 C++
C语言第三十二弹---自定义类型:联合和枚举
C语言第三十二弹---自定义类型:联合和枚举
|
8月前
|
存储 编译器 C语言
【C++初阶(四)aoto关键字与基于范围的for循环】
【C++初阶(四)aoto关键字与基于范围的for循环】
51 0
|
11月前
|
存储 C语言
C语言学习第四课——一些关键字,操作符以及数组与指针的介绍
C语言学习第四课——一些关键字,操作符以及数组与指针的介绍
55 0
|
存储 编译器 Linux
C语言深度剖析:关键字
本篇内容既是对之前C语言学习的重点复习内容,也是对之前内容的补充,大体内容逻辑参考了《C语言深度解剖》这本书,所以这节内容更像是个对C语言学习的一个重点内容复盘,也就是将C语言重点知识重新串一遍,同时也对之前一些掌握不太好的知识的补充。所以下面几乎都是一个一个的知识点,友情提示:不是很适合零基础的同学。基础不太好的请将C语言的基础内容部分系统进行学习。
C语言深度剖析:关键字
|
C语言
【c语言进阶】三分钟教会你终极套娃:指向函数指针数组的指针,让你真正理解如何解读一个变量的类型
【c语言进阶】三分钟教会你终极套娃:指向函数指针数组的指针,让你真正理解如何解读一个变量的类型
71 0
|
Java
关于关键字volatile的一二
关于关键字volatile的一二
56 0
|
安全 编译器 C语言
【C语言】刨根问底 - 深剖const关键字
【C语言】刨根问底 - 深剖const关键字
74 0
【C语言】刨根问底 - 深剖const关键字
|
C语言
初学C语言的福音-初识关键字和指针(二)
初学C语言的福音-初识关键字和指针
58 0
初学C语言的福音-初识关键字和指针(二)
|
存储 编译器 C语言
初学C语言的福音-初识关键字和指针(一)
初学C语言的福音-初识关键字和指针
74 0
初学C语言的福音-初识关键字和指针(一)