1.const的作用
const
关键字用于声明一个变量,其值在初始化后不能被修改。这提供了一种在编译时检查变量值是否被意外修改的方法,增加了程序的可靠性和安全性。const
可以用于基本数据类型、指针以及函数参数。
修饰常量
正常定义一个const修饰的变量:
//const.c
#include <stdio.h>
const int value_a = 100;
int main(void)
{
printf("The value of value_a is: %d\n", value_a);
return 0;
}
//The value of value_a is: 100
尝试去修改value_a的值:
结果是直接不可以编译。
修饰指针指向的量
正常来说通过指针可以修改变量的值
//const.c
#include <stdio.h>
int main(void)
{
int a = 10;
int *p1 = &a; // p1是指向常量整数的指针,不能通过p1修改a的值
*p1 = 20;
printf("The value of p1 is: %d\n", *p1);//The value of p1 is: 20
return 0;
}
在类型前加了const后,表明是常量类型,可以用指针指向它,但不能修改它,因为它现在是只读的,如果修改就会报错,提示这个指针是只读的。
修饰指针
//const.c
#include <stdio.h>
int main(void)
{
int a = 10;
int *const p2 = &a; // p2是常量指针,指向整数a,不能修改p2的指向,但可以通过p2修改a的值
printf("The value of p2 is: %d\n", *p2);//The value of p1 is: 10
return 0;
}
在*后面加const是在修饰指针,表明这个指针的指向就不可以变了,所以叫常量指针。
尝试修改p2的指向:
可以看到,不可以进行修改。
修饰常量和指针
可以同时用const把指针的指向和指针都修饰了,这样既不可以通过指针改值,也不可以改指针的指向。
//const.c
#include <stdio.h>
int main(void)
{
int a = 10;
const int *const p3 = &a; // p3是常量指针,指向常量整数,既不能修改指向,也不能通过p3修改a的值
printf("The value of p2 is: %d\n", *p3);//The value of p2 is: 10
return 0;
}
这样做就不可以在函数内部修改参数指向的值了:
void func(const int *ptr) {
// 函数内部不能通过ptr修改它所指向的值
}
2.置1和置0操作
unsigned int num = 0x23CF;
num |= (1 << 9);//从右往左数,第10位置1
num &= ~(1 << 9);//从右往左数,第10位置0
3.运算符
诸如:[]、()、.(前面有个点)、->、-、++、--、*、(类型)(强制类型转换)、sizeof、/、%、+、>、>=、<、<=、==、!=、&&、||、?:、=、/=、*=、%=、+=、-=、<<=、>>=、&=、|=、,,这些运算符常见,不做介绍。
3.1 !(逻辑非运算符)
int a;
int b = !a;
!是单目运算符,右边的操作数非零,那就会得到0;如果是0,就会得到1。
3.2 左移、右移
#include <stdio.h>
int main(void)
{
int a = 10;
int b = a >> 2;
printf("The value of b is: %d\n", b);//The value of b is: 2
return 0;
}
这里a是以十进制表示的,但追其根本,是二进制数。左移和右移操作,就是在将数的二进制整体往左或往右移动。
10的二进制是1010,右移两位后,成了0010,也就是十进制的2。
所以,如何填充,是有规则的。
数分为有符号数和无符号数。
无符号数
左移:
#include <stdio.h>
int main(void)
{
unsigned char a = 0xFF;
unsigned char b = a << 2;
printf("The value of b is: %d and %X\n", b, b);//The value of b is: 252 and FC
return 0;
}
直接丢掉了最前面的数。
右移:
#include <stdio.h>
int main(void)
{
unsigned char a = 0xFF;
unsigned char b = a >> 2;
printf("The value of b is: %d and %X\n", b, b);//The value of b is: 63 and 3F
return 0;
}
直接丢掉了最后面的数。
有符号数
左移:
//move.c
#include <stdio.h>
int main(void)
{
signed char num = 101;
num = num << 2;
printf("The value of num is: %d\n", num);//The value of num is: -108
return 0;
}
为什么结果是-108呢?
101的二进制是0110_0101,左移两位后,是1001_0100。
num是个有符号数,计算机中,有符号数是用补码表示的,最高位为1,则表示负数,最高位为0,则表示负数。这样做只是一种表示,想要知道对应的十进制是多少,就需要转化。
所以,按照补码转原码的规则,1001_0100先取反得:0110_1011,再加1得:0110_1100,这是二进制数对应的十进制是:108。而且并不是说转化后就不是负数了,num原本就是个负数,所以num为-108。
右移:
//move.c
#include <stdio.h>
int main(void)
{
signed char num = 101;
num = num >> 2;
printf("The value of num is: %d\n", num);//The value of num is: 25
return 0;
}
右移就成了:0001_1001,十进制就是25。
按位与&
按位与表示两个数的每一个对应bit位进行与操作。
#include <stdio.h>
int main() {
unsigned char a = 0b10101010; // 170的二进制表示
unsigned char b = 0b01010101; // 85的二进制表示
unsigned char result = a & b;
printf("The result of %d (&) %d is: %d\n", a, b, result);
printf("In binary, the result is: %b\n", result);
return 0;
}
两个都是1结果才为1,其他情况都是0。
如果一个数只有8个bit,另一个有32个bit,那么缺的部分,是按0处理的。
//and.c
#include <stdio.h>
int main(void)
{
unsigned char num = 101;
unsigned int numa = 33002334;
unsigned int result = num & numa;
printf("The value of result is: %d\n", result);//The value of num is: 68
return 0;
}
也就是说,虽然num只要8位,但在跟32位的numa进行与操作时,没有的那些位,都是在用0在跟numa进行与操作。从结果中也可以看出,很明显numa的8位前的那些数,都被置0了。
或操作 | 和异或^,操作也是一个道理。
优先级
运算符是具有优先级的,指的是在同一个表达式中,应该先执行哪个符号的运算。而优先级具体是多少,可以查阅c语言运算符优先级表。
需要注意的是,*具有两个意义,一个是取值运算符,一个是乘,用作取值时的优先级是要高于乘的。同样的还有&,一个是取地址,一个是按位与;还有-,负号和减。
另外,当优先级同级时,如何判断运算次序,就需要根据结合性来判断。结合性有两种,一种是从左到右,一种是从右到左。
从左到右:就是计算次序从左往右走。
//combine.c
#include <stdio.h>
int main(void)
{
int a=1,b=2,c=3;
int d = (a,b,c);
printf("The value of d is: %d\n", d);//The value of num is: 3
return 0;
}
这里的逻辑就是:括号里的值一开始是1,然后是2,最后是3,所以最终把3赋值给了d。
从右到左:就是计算次序从右往左走。
在所有的运算符中,大多数的运算符的结合性都是从左到右。从右到左的有单目运算符,条件运算符,赋值运算符=,和那些复合运算符(比如*=)。先判断优先级,再判断结合性。
[]的优先级最高,然后是();然后是成员选择,.(点)要高于->;过后是单目运算符,依次是-,~,++,--,*,&,!,类型转换,sizeof。最后两个不是单目运算符。所以自增要高于自减,取值要高于取地址。然后是/,*,%,所以除要高于乘。然后是+,-,所以加要高于减。然后是<<,>>,所以左移高于右移。然后是>,>=,<,<=,所以大于高于小于。然后是==,!=,所以等于高于不等于。然后是&,^,|,所以是按位与高于按位异或高于按位或。然后是&&,||,所以逻辑与高于逻辑或。然后是条件运算符。然后是赋值运算符,然后是复合运算符,其优先级根据组合的符号来定。最后是逗号运算符。
void (*funcPtr)(int) = print;
void *funcPtr(int) = print;
// 指针数组,每个指针指向一个函数
int (*func_ptr_array[])(int) = {add, subtract, multiply};
有了优先级和结合性后,才能分析上面的写法。
()的结合性很高,所以整体来说,这一定是在声明一个指针,然后(int)是在声明函数,所以这是在声明一个指针,并且让这个指针指向一个函数。
而如果去掉(),因为()的存在,就是先声明一个函数,函数名是funcPtr,参数一个int,然后才是返回值是void *类型。
所以说,要想声明一个指针数组,每个指针指向一个函数。首先是*func_ptr_array[],这是一个指针数组,而(*func_ptr_array)[]是一个指向数组的指针。让每个指针指向一个函数,自然就是在后面加一个(int)就可以了。
4.二维数组,行列数判断
int two_dim[][5] = {{0,1,2},{1,11},{2,21,22,23}};
像这样的没有指定行数初始化语句,会根据列数的多少来判断行数,所以two_dim的列数是3。
而行数剩余的成员,比如{0,1,2},剩余的两个成员会被初始化为0。
可以通过下面的代码得到行数和列数:
#include <stdio.h>
int main(void)
{
int two_dim[][5] = {{0,1,2},{1,11},{2,21,22,23}};
int rows = sizeof(two_dim) / sizeof(two_dim[0]);
int cols = sizeof(two_dim[0]) / sizeof(two_dim[0][0]);
printf("Rows: %d\n", rows);
printf("Cols: %d\n", cols);
return 0;
}
5.volatile关键字
代码是需要编译后才变成可执行程序的,编译是由编译器进行的,而volatile修饰了某个变量后,就是在告诉编译器,这个变量可能会在发生意想不到的改变。程序在执行时,操作系统会为程序在内存中分配一些空间,用来存代码、变量等数据。一般来说,对于一些在程序执行时经常用到的变量,会把这个变量的值存在寄存器中,当程序要读取这个变量对应的值时,就直接从寄存器中读取,这样运行的速度就会快一些。而如果在多线程等情况下,这个变量的值就可能发生变化,也就是有可能会被其他线程修改。而当前线程又不想要被修改后的值,那么为了获取到想要的值,就需要去到内存中读值。在volatile修饰后,就不会从寄存器中读取变量的值,而是去到内存中进行读取,内存中读的值,就是当前线程设定的值。
volatile可以和const一起使用,同时修饰一个变量。
6.全局变量与局部变量重名
全局变量可以和局部变量重名,局部变量会屏蔽局部变量。
对于一些编译器而言,函数内的循环体内定义的变量,也可以屏蔽局部变量。就像这个程序一样:
#include <stdio.h>
void fun(void)
{
int a = 10;
for(int a = 0; a < 10; a++)
{
printf("a is:%d\n", a);
}
printf("a is:%d\n", a);
}
int main()
{
fun();
return 0;
}
/*
a is:0
a is:1
a is:2
a is:3
a is:4
a is:5
a is:6
a is:7
a is:8
a is:9
a is:10
*/
还可以这样:
void fun(void)
{
int a = 10;
for(int a = 0; a < 10; a++)
{
printf("a is:%d\n", a);
}
printf("a is:%d\n", a);
{
int a = 30;
printf("a is:%d\n", a);
}
printf("a is:%d\n", a);
}
/*
a is:10
a is:30
a is:10
*/
所以可以看出,{}代码块中定义的变量,生命周期是从代码块的开始到结束。
7.&&和||
&&和||叫做半路与和半路或,意思是说如果第一个操作数已经可以判断出表达式的结果的话,就不需要再看第二个操作数是怎样的了。
cond1 && cond2 || cond3
先判断cond1,为假,则表达式为假,后面的就不需要进行判断了;为真,判断cond2,cond2为真,则表达式为真;cond2为假,则需要判断cond3,cond3为真,则表达式为真,cond3为假,则表达式为假。
8.字符数组的初始化
char b[2][3] ={{"d","e","f"},{"a","b","c"}};
这样写是错误的,因为""中的是字符串,比如"d",除了字符d外,其中还包含一个\0。所以其也代表一个维度,而用二维数组去接收,显然是不对的。
应该这样:
char b[2][3][2] ={{"d","e","f"},{"a","b","c"}};
所以说,{}代表一个维度,""也代表一个维度,一共三个维度,所以需要三维数组来进行接收。
9.数组的首地址
#include <stdio.h>
int main()
{
int a[6] = {1,2,3,4,5,6};
printf("(&a):%p\n",(&a));
printf("*(&a):%p\n",*(&a));
printf("**(&a):%d\n",**(&a));
printf("*((int*)(&a+1)- 1):%d\n",*((int*)(&a+1)- 1));
printf("(int*)(&a+1):%d\n",(int*)(&a+1));
printf("((int*)(&a+1))+1:%d\n",((int*)(&a+1))+1);
return 0;
}
/*
(&a):0x7ffe2031ddc0
*(&a):0x7ffe2031ddc0
**(&a):1
*((int*)(&a+1)- 1):6
(int*)(&a+1):540138968
((int*)(&a+1))+1:540138972
*/
&a是取a的地址,a的地址是数组整体的入口地址,也就是数组的首地址。
同时它也是数组第一个元素的地址,也就是元素1的地址,所以可以看到,数组的地址&a和0号元素的地址的值是一样的。但是很明显,要想访问0号元素,就需要进一步地解引用,**(&a)执行地过程就是先得到数组a的首地址,然后解引用得到数组a的0号元素的地址,再解一次,才能得到0号元素的值。
而&a+1,则是在数组首地址的基础上+1,意思是去到这个数组的后面一位,也就是元素6的地址的下一个地址。所以说呢,*((int*)(&a+1)- 1),就是先去到了数组末地址的后一位,然后强制类型转换成int*类型,也就是对其进行了一次解引用,这样就得到了数组后面的一位的地址值,然后-1又回到了数组末地址,因为地址值是逐渐增大的,最后再解引用,得到的值就是6。
这个代码的关键点是,要清楚如果处于数组地址这一层,对其进行加减,是以数组的大小为单位进行的,比如这里的数组大小是24字节,加1就是在内存中偏移24个字节,加2就是偏移48个字节。对其解引用后,加1就是加一个int大小,减1就是减去一个int的大小。
#include <stdio.h>
int main()
{
int a[6] = {1,2,3,4,5,6};
printf("(&a):%p\n",(&a));
printf("(&a+1):%p\n",(&a+1));
printf("(&a+2):%p\n",(&a+2));
printf("((int*)(&a)):%p\n",((int*)(&a)));
printf("(((int*)(&a))+1):%p\n",(((int*)(&a))+1));
return 0;
}
/*
&a):0x7fffdc541f20
(&a+1):0x7fffdc541f38
(&a+2):0x7fffdc541f50
((int*)(&a)):0x7fffdc541f20
(((int*)(&a))+1):0x7fffdc541f24
*/
从上面的代码可以看出结果是正确的。
究其本质,a是一个大小为6的int型数组,定义之后,在内存中会有一块连续的存储空间来存这个数组,大小就是4x6=24字节。而a是数组名,也可以当作指针用,本质上就是一根指针指向的首地址,那么在定义的时候就已经确定了a是一个占24字节的数组,对其进行偏移,偏移一个单位肯定就是偏移24个字节。对其解引用,则是通过a这个指针找到数组的首地址,所以这时再+1,偏移的就是一个int的大小。
10.字符串转换宏#
#include <stdio.h>
#define PRINT_MESSAGE(message) \
printf("%s\n", #message);
#define PRINT_STRING "Hello, World!"
int main()
{
PRINT_MESSAGE(111);
PRINT_MESSAGE(PRINT_STRING);
return 0;
}
/*
111
PRINT_STRING
*/
#这个宏可以把宏的参数转换成字符串,所以可以看到,111转递刚message后,再由#转换成字符串,然后被打印出。仅对宏的参数有效果,所以可以看到,#后面的是个宏,但它不会解析这个宏,而是直接将这个宏的名字转换成字符串。