【C】浅析C的那些事儿

1、sizeof

   sizeof是一个计算“变量”(参数)所占空间大小的运算符,其参数可以是变量、指针、数据类型、数组名、结构体、函数、对象等等,其值在编译时就已经计算好了。为什么说sizeof是一个运算符,而不是一个函数呢?先来看看下面的代码:

int main(void)
{
	int a = 0;

	printf("%d\n", sizeof a);//4
	printf("%d\n", sizeof(a));//4
	printf("%d\n", sizeof int);//error
	printf("%d\n", sizeof(int));//4

	return 0;
}

        在32位机上运行上述代码,发现sizeof a和 sizeof(a)的计算结果均为4,而函数一般是fun(……)这样表示的。仔细想想,函数名后面没有括号合适吗?显然是不合适的,函数名后面必须要有括号,这恰恰说明了sizeof不是一个函数,而是一个运算符,并且sizeof在计算变量的空间大小时括号可以省略。sizeof int运行时报错,而sizeof(int)运行结果为4,这说明了sizeof在计算数据类型大小时不能省略括号。(建议:不要省略括号)

int main(void)
{
	short s = 3;
	int a = 10;

	printf("%d\n", sizeof(s = a + 2));//2
	printf("%d\n", sizeof(s = a));//2
	printf("%d\n", s);//3

	return 0;
}

       运行上述代码,发现sizeof(s=a+2)和sizeof(s=a)的结果是相同的,s的值没有发生变化,这恰恰说明了sizeof(……)内部的表达式是不参与运算的因为在编译阶段已经为它分配好了内存。接下来总结几点有关使用sizeof时的注意事项:

a. sizeof(数组名),数组名表示整个数组,其求取的是整个数组的空间大小;

b. &数组名,数组名表示整个数组,其取出的是整个数组的地址;

c. 除a,b外,遇到的数组名都指的是数组首元素的地址(ps:二维数组名a表示一维数组a[0],也就是第0行的首地址)。

2、strlen

   strlen(……)是一个函数,程序运行过程中才能计算它的值。其参数必须是char *类型(也就是比如传入地址),它用来返回字符串的实际长度。假如一些字符存放在字符数组中,那么数组元素应该包含'\0',否则计算strlen的值将会出现问题。

int main(void)
{
	char str[] = {'l', 'o', 'v', 'e'};

	printf("%d\n", strlen(str));//11

	return 0;
}

       运行上述代码,输出结果为:


   我们预期的结果是输出4,为什么会输出11呢?这是因为strlen函数在计算字符串实际长度时,遇到第一个'\0'就会结束计算,而上述代码并没有赋给字符数组'\0',这时候它会一直寻找,直至找到'\0'为止,因此这个结果是一个不确定的值,这是一个相当危险的操作。

3、示例

   接下来通过多组计算来巩固下sizeof和strlen的区别:

a.一维数组

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

printf("%d\n",sizeof(a));//16 sizeof(数组名)表示计算整个数组的空间大小4*4=16
printf("%d\n",sizeof(a+0));//4 表示数组a中第0个元素的地址,32位机共有32根地址总线,
                           //因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(*a));//4 表示数组a中第0个元素的值,该数组中的元素为整型,而整型占4个字节
printf("%d\n",sizeof(a+1));//4 表示数组a中第1个元素的地址,32位机共有32根地址总线,
                           //因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(a[1]));//4 表示数组a中第1个元素的值,该数组中的元素为整型,而整型占4个字节
printf("%d\n",sizeof(&a));//4 &数组名表示整个数组的地址,32位机共有32根地址总线,
                          //因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(*&a));//16 表示解引用整个数组,即就是整个数组的空间大小4*4=16
printf("%d\n",sizeof(&a+1));//4 表示移动一次整个数组的地址,32位机共有32根地址总线,
                            //因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(&a[0]));//4 表示取第0个元素的地址,32位机共有32根地址总线,
                             //因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(&a[0]+1));//4 表示取第1个元素的地址,32位机共有32根地址总线,
                               //因此地址所占内存大小为32/8=4

b.字符数组

char arr[] = {'a','b','c','d','e','f'};

printf("%d\n", sizeof(arr));//6 没有结束符'\0',因此值为6
printf("%d\n", sizeof(arr+0));//4 表示第0个元素(即就是'a')的地址,32位机共有32根地址总线,
                              //因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(*arr));//1 表示解引用数组arr的首元素地址,即就是'a',该数组的元素类型为char,占1个字节
printf("%d\n", sizeof(arr[1]));//1 表示数组第1个元素,即就是'b',该数组的元素类型为char,占1个字节
printf("%d\n", sizeof(&arr));//4 表示整个数组的地址,32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(&arr+1));//4 表示移动一次整个数组的地址,32位机共有32根地址总线,
                               //因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(&arr[0]+1));//4 表示取第1个元素(即就是'b')的地址,32位机共有32根地址总线,
                                  //因此地址所占内存大小为32/8=4

printf("%d\n", strlen(arr));//随机值 表示从首元素开始,计算字符串长度,该字符数组没有'\0',
                            //这时候它会一直寻找,直至找到'\0'为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(arr+0));//随机值 表示从首元素开始,计算字符串长度,该字符数组没有'\0',
                              //这时候它会一直寻找,直至找到'\0'为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(*arr));//error 表示解引用首元素(第0个元素)地址,实际上就是首元素'a',
                             //而传入strlen()函数的参数是地址,也就是传入字符‘a’的ASCII值,显然这是非法的 
printf("%d\n", strlen(arr[1]));//error 表示第1个元素'b',实际上传入strlen()函数
                               //的参数是地址,也就是传入字符‘b’的ASCII值,显然这是非法的
printf("%d\n", strlen(&arr));//随机值 表示整个数组地址,该字符数组没有'\0',这时候它会一直寻找,
                             //直至找到'\0'为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(&arr+1));//随机值 表示移动一次整个数组的地址,该字符数组没有'\0',
//这时候它会一直寻找,直至找到'\0'为止,因此这个结果是一个不确定的值,与strlen(arr)计算出的随机值比较相差6
printf("%d\n", strlen(&arr[0]+1));//随机值 表示从数组第一个元素(即就是'b')开始,计算字符串长度,
//该字符数组没有'\0',这时候它会一直寻找,直至找到'\0'为止,因此这个结果是一个不确定的值,
//与strlen(arr)计算出的随机值比较相差1
char arr[] = "abcdef";

printf("%d\n", sizeof(arr));//7 字符串包含'\0',计算整个数组的大小,因此值为7
printf("%d\n", sizeof(arr+0));//4 表示第0个元素的地址,32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(*arr));//1 表示解引用首元素的地址(即就是'a'),元素'a'的类型为char,占用1个字节
printf("%d\n", sizeof(arr[1]));//1 表示第一个元素(即就是'b'),元素'b'的类型为char,占用1个字节
printf("%d\n", sizeof(&arr));//4 表示整个数组的地址,32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(&arr+1));//4 表示移动一次整个数组的地址,32位机共有32根地址总线,
                               //因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(&arr[0]+1));//4 表示第1个元素(即就是'b')的地址,32位机共有32根地址总线,
                                  //因此地址所占内存大小为32/8=4

printf("%d\n", strlen(arr));//6 arr表示数组的首地址,字符串包含结束符'\0',所以字符串长度为6
printf("%d\n", strlen(arr+0));//6 表示第0个元素的地址,字符串包含结束符'\0',所以字符串长度为6
printf("%d\n", strlen(*arr));//error 表示解引用首元素(第0个元素)地址,实际上就是首元素'a',
                             //而传入strlen()函数的参数是地址,也就是传入字符‘a’的ASCII值,显然这是非法的
printf("%d\n", strlen(arr[1]));//error 表示解引用第1个元素地址,实际上就是首元素'b',
                               //而传入strlen()函数的参数是地址,也就是传入字符‘b’的ASCII值,显然这是非法的
printf("%d\n", strlen(&arr));//6 表示整个数组的地址,字符串包含结束符'\0',所以字符串长度为6
printf("%d\n", strlen(&arr+1));//随机值 表示移动一次整个数组,但是这样会跳过'\0',这时候它会一直寻找,
                               //直至找到'\0'为止,因此这个结果是一个不确定的值
printf("%d\n", strlen(&arr[0]+1));//5 表示第1个元素的地址,也就是从'b'开始计算其字符串长度,所以字符串长度为5
char *p = "abcdef";

printf("%d\n", sizeof(p));//4 p是指针变量,指的是字符串的首元素(即就是'a')的地址,32位机共有32根地址总线,
                          //因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(p+1));//4 p+1指的字符串的第1个元素的地址,32位机共有32根地址总线,
                            //因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(*p));//1 表示解引用p的首地址,即就是'a',char型占用1个字节
printf("%d\n", sizeof(p[0]));//1 表示第0个元素(即就是'a'),char型占用1个字节
printf("%d\n", sizeof(&p));//4 &p表示指针变量的地址,p是个二级指针,也可以理解为地址的地址,
                           //32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(&p+1));//4 表示跳过一个指针变量大小的指针,32位机共有32根地址总线,
                             //因此地址所占内存大小为32/8=4
printf("%d\n", sizeof(&p[0]+1));//4 表示第1个元素(即就是'b')的地址,32位机共有32根地址总线,
                                //因此地址所占内存大小为32/8=4

printf("%d\n", strlen(p));//6 表示从首地址开始(即就是'a')计算其字符串长度,包含'\0',因此值为6
printf("%d\n", strlen(p+1));//5 表示从第1个元素地址开始(即就是'b')计算其字符串长度,包含'\0',因此值为5
printf("%d\n", strlen(*p));//error *p指的是'a',而传入strlen函数的是'a'的地址,也就是'a'的ASCII值,这是非法的
printf("%d\n", strlen(p[0]));//error p[0]指的是'a',而传入strlen函数的是'a'的地址,
                             //也就是'a'的ASCII值,这是非法的
printf("%d\n", strlen(&p));//随机值 &p表示存放p的地址,这是一个未知的值,不知道'\0'在哪里,因此这是一个不确定的值
printf("%d\n", strlen(&p+1));//随机值 表示跳过一个指针变量大小的指针,与上一个随机值不一定差4,
                             //因为有可能提前出现'\0'
printf("%d\n", strlen(&p[0]+1));//5 表示从第1个元素(即就是'b')的地址开始,计算字符串长度为5

c.二维数组

int a[3][4] = {0};

printf("%d\n",sizeof(a));//48 a表示整个数组,所占内存空间大小为4*3*4=48
printf("%d\n",sizeof(a[0][0]));//4 表示第0行第0列的元素,数组元素类型为整型,因此占4个字节
printf("%d\n",sizeof(a[0]));//16 表示第0行的数组名,实际上是有4个元素的一维数组,所占内存为4*4=16
printf("%d\n",sizeof(a[0]+1));//4 表示第0行第1列的地址,32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(a+1));//4 表示第1行的地址,32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(&a[0]+1));//4 表示第1行的地址,32位机共有32根地址总线,因此地址所占内存大小为32/8=4
printf("%d\n",sizeof(*a));//16 表示解引用数组第0行的地址,也就是a[0]这个一维数组的大小,因此占用内存为4*4=16
printf("%d\n",sizeof(a[3]));//16 表示第3行的地址,sizeof()内部不参与运算,因此值为4*4=16,
                            //切记不要认为数组越界就不计算占用内存大小

d.函数

void Func(char str[100]) //void Func(char * str)
{
	return;          //sizeof(str) = 4,str是个指针变量,本质上是个地址
}

void *p = malloc(100);   //sizeof(p) = 4,p是个指针变量,本质上是个地址

4、static

   a. static修饰局部变量,可以改变变量的生命周期,还会修改变量的存储类型(栈区->静态区),并且会保留上一次计算的值,直到程序运行结束才被销毁,但是并不会改变变量的作用域;

   b. static修饰全局变量或者函数,其只能在本文件中使用,即使在别的文件中用extern声明也不能使用,其本质是外部链接属性(全局变量具有外部链接属性)变成了内部链接属性(被static修饰的变量具有内部链接属性)。

   c. static修饰形式参数,其存储于栈区,作用域是整个函数,函数只有在被调用的时候才会把实参传递给形参,假如形参被static修饰了,那么就会在编译的时候为其分配内存,假如该函数从未被调用,那么分配的这些内存就毫无意义,这样其实是在浪费内存,因此这是不允许的。

   注意:static int a = 0;在编译的过程中,通过反汇编查看,发现程序会直接跳过这段代码。

5、const

   const修饰的是常变量,具有常量的属性,同时它又可以修饰变量。

const int num = 5;
num = 10;//error const修饰变量num,但是num不能被改变,这说明const具有常属性
const int n = 5;
int arr[n] = {0};//error 既然const具有常属性,那么测试下n是否可以作为数组的大小,
                 //结果是否定的,这说明了const修饰的是变量而不是常量

   const修饰指针变量(看谁距离const最近

   当const放在*左边时,可以将*左边的数据类型(比如说,int)通通用笔划掉,查看const距离谁最近,只留下const * p,这就表示p所指向的内容不能改变,而指针变量p是可以被改变的。

int const * p;
const int * p;//两者等价,指针变量p指向的内容不能通过指针来改变,但是指针变量本身可以改变

   运行下面的代码,发现*p不可以被改变,而p可以被改变。

int main(void)
{
	int num = 10;
	int n = 20;

	const int * p = #//编译报错,左值指定const对象
	p = &n;//const位于*左边时,p可以被改变
	*p = 20;

	return 0;
}

   当const放在*右边时,可以将*左边的数据类型(int)通通用笔划掉,查看const距离谁最近,只留下const p,这就表示p所指向的内容可以被改变,而指针变量p是不能被改变的。

int * const p;//指针变量p不能被改变,但是指针变量指向的内容可以通过指针改变

   运行下面的代码,发现p不可以被改变,而*p可以被改变。

int main(void)
{
	int num = 10;
	int n = 20;

	int * const p = #
	p = &n;//const位于*右边时,p不可以被改变
	*p = 20;

	return 0;
}

   接下来通过一段故事来生动地讲解下const修饰指针变量以及变量的过程中需要注意的事情,需要提前说明的是,被const修饰的指针变量或者变量都不能改变。


const int num = 10;
int n = 20;
const int * p = #
*p = 20;
p = &n;

   num、n均是男生,p是女生,当num把它的地址赋给p时,也就是num与p之间建立了一种关系,称之为“男女朋友关系”。int * const p = &num可以这样理解,男生num说我兜里只有10块钱(const int num=10),不能请你吃饭,女生p心想你都不肯花钱请我吃饭,我得换个男朋友。既然男生num不想花钱请女生吃饭,那么*p(ps:即就是num)不能变为0(当然*p也不能改变为其他值)。const修饰*p,此时p可以改变,于是女生p就说我要和男生n交往,因为男生n有钱(ps:n有20块钱),这时候女生p就与男生n建立了“男女朋友关系”(ps: p = &n),从而断开了与num的联系。

const int num = 10;
int n = 20;
int * const p = #
*p = 20;
p = &n;

   当const放在*右边,p的左边时,这意味着p不能改变,也就是说女生p不能再与男生n建立“男女朋友关系”,这时候*p可以被改变,女生p就对男生num说我可以不换男朋友,但是你得请我吃饭,然后男生num就答应了。

const int num = 10;
int n = 20;
const int * const p = #
*p = 20;
p = &n;

   const int * const p;由于const不仅修饰p,还修饰*p,这就意味着女生p既不能换男朋友num,而她男朋友num又可以不请她吃饭。好惨啊,哈哈……

6、volatile

   编译时不优化,执行时不缓存,并且被volatile修饰的变量只能去内存中读取,而不是在寄存器中读取,它直接存取原始内存地址,保证了内存的可见性。除此之外,volatile还用于多线程和多CPU编程。

const int num = 10;
int * p = (int *)#
*p = 20;//即使num被const修饰,但是仍然可以通过地址去改变num的值,这是不安全的,
//将这段代码保存为test.c文件并在vs中运行,num的值会被改变,若将这段代码保存为test.cpp文件并运行,
//发现num的值不会被改变,而如果在Linux中运行.c文件(gcc test.c -O2),num的值不会被改变,
//这是因为编译器不同而对其作出的不同的处理(或者说是一种优化),num的值如果一直被使用,
//编译器就有可能把num的值存放到寄存器中,便于操作,毕竟寄存器的读取效率更高一些,
//假如这时候编译器已经把num=10保存在了寄存器中,下次访问的时候会直接去寄存器中读取,
//虽然通过指针操作改变了num的值为20,但是由于不会去内存中访问它,就会造成num的值没有改变。
//到底从寄存器去读取这个值还是在内存中去读取,这是一个问题?

volatile const int num = 10; //加上volatile关键字后,编译器在编译时不做出优化,
                             //会保证在内存中读取num的值,而不是在寄存器中,
                             //这样保证了内存的可见性。

a.一个变量可以既是const又是volatile吗?

   可以,比如说,只读的状态寄存器。

b.一个指针可以被volatile修饰吗?

   可以,比如说,当一个中断服务子程序修改一个指向buffer的指针时,就可以是volatile。

猜你喜欢

转载自blog.csdn.net/sustzc/article/details/79835876
今日推荐