文章目录
- 对函数参数中的指针使用malloc容易犯的错误
- 用scanf读取包含空格的字符串
- 优先级最低的运算符——逗号运算符
- `enum`枚举类型的元素默认按0,1,2,... 赋初始值
- `char a = '好';` 错误的写法
- `func()` 等价于 `int func()`
- 向下取整是数值部分向下取整,与正负号无关
- `0 || -1`的值是1
- &arr , arr , &arr[0] 的区别
- `#include` 的两种写法的区别:
- switch case语句后面要加break
- 常量字符串的声明: **`const char * p` 和 `char *(const p):`**
- 字符串&字符数组:
- 未经初始化的数组内容是内存中的不可预测数据
- 巧用三元运算符
- 宏定义函数
- 回调函数
- 以数组作为形参的函数的声明方式
- Visual Studio出现_CRT_SECURE_NO_WARNINGS警告
- 安全地快速初始化无穷大
-
对函数参数中的指针使用malloc容易犯的错误
如果你需要在一个函数内对传入的指针使用
malloc
来分配内存,那么你需要传这个指针的指针,或者传这个指针的引用。请看下面的例子:void fun(char * data) { data=(char *)malloc(10*sizeof(char)); ... } int main() { char * s; fun(s); }
在main函数中声明的
s
其实没有任何变化,因为data
并不是s
本身,传参的时候data
是另一个指针然后指向了s
所指的内容,本来是没有问题的,但是malloc
是重新给data指针分配了地址,而并没有给外面的s
重新分配地址,从此data
和s
没有任何关系了。正确的做法是void fun(char * &data) { data=(char *)malloc(10*sizeof(char)); ... }
-
用scanf读取包含空格的字符串
我们都知道scanf读取字符串时,遇到空白字符(空格或制表符)就会停止。
而用以下方法可以读取包含空格的字符串,遇到换行符才停止:scanf( "%[^\n]", s);
^
是表示补集的正则表达式,即除了\n
全部匹配。
-
优先级最低的运算符——逗号运算符
表达式1
,
表达式2先计算左边的操作数,再计算右边的操作数。右操作数的类型和值作为整个表达式的结果。
由于逗号运算符的优先级最低,因此下面的语句运行之后:int a=(1,2,3); int b=1,2,3;
a的值是3,b的值是1
括号改变了运算优先级导致先计算表达式1,2,3
,因此a等于右操作数3
-
enum
枚举类型的元素默认按0,1,2,… 赋初始值enum DAY { MON,TUE,WED,THU,FRI,SAT,SUN };
这样定义的枚举类型DAY中MON的值是0,TUE的值是1,以此类推
如果:enum DAY { MON,TUE,WED=10,THU,FRI,SAT,SUN };
这样给某个元素赋初值,那么MON=0, TUE=1, WED=10, THU=11, FRI=12, SAT=13, SUN=14
即,没有人为赋值的元素的值是上一个元素的值+1
-
char a = '好';
错误的写法char
类型的值占用1字节(Byte), 而一个汉字占用2字节,因此单个汉字不是字符而是字符串,要写:char a[]="好";
这个字符数组占用3字节(一个汉字2字节,结束标识\0
1字节) 这个字符串的长度为2
但是在UTF-8编码中,一个汉字占3字节,
GCC编译器默认使用UTF-8:char a[]="好"; printf("%d",strlen(a));
输出:3
而在UTF-16中一个汉字占4字节。除此之外的主流编码中,一个汉字都是2字节
-
func()
等价于int func()
定义函数没有指定函数返回值类型时,默认返回
int
值,因此下面这种写法是合法的:func() { return 1; }
但是会引发warning:
因此并不推荐这种写法
在逻辑表达式中,除了0是false(0),非0数值都是true(1)
int arr[10];
&arr
, arr
, &arr[0]
三者的值是完全相等的
可以认为 arr
和 &arr[0]
是等价的,都是指数组首元素即arr[0]的地址,是int*
类型的数据
而&arr是指整个数组的起始地址,虽然值和首元素的地址相等,但类型不同,&arr是int**
类型的数据,即指向数组的指针类型,因此,当我们把三者分别加1时:
int arr[10];
printf("%d\n",arr);
printf("%d\n%d\n%d",&arr[0]+1,arr+1,&arr+1);
结果:
6421984
6421988
6421988
6422024
可以看出,&arr[0]+1
和arr+1
表示的地址都只偏移了4个字节,而&arr+1
表示的地址偏移了4*10=40个字节
-
#include
的两种写法的区别:#include <stdio.h> 表示在系统的C头文件默认存储路径中寻找头文件stdio.h
#include "stdio.h" 表示在当前目录(源文件所在路径)中寻找头文件stdio.h
也就是说,第三方头文件得放在源码文件所在的目录下,并使用第二种方式引用
int a=2;
switch(a)
{
case 1:
printf("1");
case 2:
printf("2");
case 3:
printf("3");
case 4:
printf("4");
break;
case 5:
printf("5");
}
输出:
234
-
常量字符串的声明:
const char * p
和char *(const p):
用const修饰的变量为常量,值不允许被改变,但如果用const修饰指针变量,则有上述两种方式,那么他们有什么区别呢?
const char * p
表示指针指向的地址内的值是常量,指针的值(指向什么地址)可以改变,指针指向的地址内的值不可以改变。即我声明了一个const char
类型的*p
,这个p
是一个const char
类型值的储存地址。并且事实上这种声明的更清晰的写法是char const * p
,这最直接地说明了*p
是const类型
比方说:我有如下定义:const char * p="abcd";
那么,
p="efgh";
是合法的,(PS:像
"efgh"
这样的值属于const char *)因为’"abcd"
这个值还在 那里,没有变,我只是新增了一个值"efgh"
,并让p指向它。
但是*p="efgh";
是非法的,对*p进行赋值就是在试图改变常量
"abcd"
而
char * const p;
则是定义了一个
char
类型的*(const p)
(去掉括号,结合方式不会变,这里只是为了看的更清楚一些),此时指针的值不可以改变,即指针不可以指向其他地址。但是指针指向的地址内的值是可以改变的
但是要注意
如此定义的指针不能指向"abcd"
这样的常量字符串,即以下写法是错误的:*p="efgh";
原因见下面的注意:
注意!char *s="abcd";
这种写法并不规范。因为"abcd"本身是属于常量类型const char *
经实验:在gcc中编译不会报错,能正常运行。但是一旦尝试更改"abcd"
的值就会抛出异常
但是在VS2019中编译时就会出现这样的错误:
所以规范的写法是const char *s="abcd";
具体含义见上文。
如果你非要这样写,为了顺利通过VS的编译,可以使用强制类型转换:
char *s = (char *)"abcd";
这样虽然不会报错,并且能被正常地读取,但s实际上仍是const char *
类型,一旦你试图更改"abcd"这个常量字符串:
就会在运行时引发异常。因为字符串常量存储在常量存储区,该区域在运行时是只读的。
如果我们的需求就是在编译时给一个字符串赋以默认值,并且在运行时可能进行修改,应该怎么写?
请继续往下看:
-
字符串&字符数组:
在C语言中没有字符串的类型(C++中有string类),但是有字符串的概念,所谓字符串(character string)是以空字符'\0'
结尾的char数组。
scanf
,gets
等函数会自动在获取的字符串结尾加上'\0'
而像这样:
用char s[]="aaa"; char s[10] = "aaa";
" "
来给char数组赋值,编译器也会在数组的末尾加一个\0
而用{ }
来赋值就不会添加\0
因此char s[]="a";
数组长度是2,char s[]={'a'};
数组长度是1
-
未经初始化的数组内容是内存中的不可预测数据
int map[10][10]; for(int i=1;i<=9;i++,putchar('\n')) for(int j=1;j<=9;j++) printf("%d ",map[i][j]);
像这样,一个数组内的元素初始值并不是0,因为声明数组的过程只是指定了一块内存空间,而并未清除该段内存原本的数据,如果直接运行会出现下面这样的结果:
用下面的方法可以快速地初始化数组:
int arr[10][10]={0};
无论数组的维数是多少,这样都可以将数组内的元素全部初始化为0.
如果把0换成其他的数比如2,那么数组的第一个元素会是2,其余元素全部初始化为0
特别的,静态数组声明时会初始化为0
static int map[10][10];
for(int i=1;i<=9;i++,putchar('\n'))
for(int j=1;j<=9;j++)
printf("%d ",map[i][j]);
运行结果:
格式:
#deine
func_name
(parameter
)expression
可以传入参数,返回expression的值
先来看一个最简单的例子:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
a和b都用括号的原因是,宏定义函数中expression的计算只是单纯的把参数填进去,而这个参数的数据类型乃至格式是没有限制的。为了保证值的独立性,用括号包裹非常保险。
再来看一个实用的例子:
#define MALLOC(n, type) ( (type *) malloc( (n) * sizeof(type) ) )
传入大小和类型名,在堆上开辟长度为n的该类型的空间并返回其指针(这里可以参考我写的另一篇C/C++程序运行的五种内存分区)
普通的函数无法做到这一点,因为数据类型是不允许作为函数参数传递的,而宏函数因其直接替换的特性却可以做到。
如果表达式想换行,在除了最后一行的每行的末尾加上续行符\
:
#define MALLOC(n, type) \
( (type *) malloc( (n) * sizeof(type) ) )
宏函数中还可以写return
,会直接替换到调用处,下面给出一个最简示例:
#define f() \
return 10
int func()
{
f();
return 1;
}
调用func函数,返回值是10而不是1,看到这里,相信你已经理解了宏定义函数的原理:简单粗暴的文本替换。
先摘一段定义:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
从知乎上看到的比较生动的解释(侵删):
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
C语言实现:
int callback(int a,int b)
{
return a + b;
}
int dispose(int(*func)(int,int))
{
int x = func(1,2);
return 10 * x;
}
int main()
{
printf("%d",dispose(callback));
}
输出:30
在上面的例子中,我们调用了dispose函数并向它传递了callback函数的地址,dispose函数接收该参数,并将传入的地址赋值给func函数指针,最后通过func成功调用callback函数。
一维数组
type func(int arr[10]);
type (int arr[]);
type (int *arr)
二维数组
type func(int **arr);
type func(int arr[][10]); //**数组的第二维维度一定要显式指定**
方法1
右击项目—>属性–>
方法2
在源文件首部添加:
#define _CRT_SECURE_NO_WARNINGS
或者
右击项目—>属性–>C/C+±->预处理器 添加
_CRT_SECURE_NO_WARNINGS
memset(arr,0x3f,sizeof(arr));
使用0x3f填充数组,每项数值足够大的同时即使乘2也不会溢出,执行之后数组元素的值为:
若是要单项的无穷大数值,可以写
const int INF = 0x7fffffff;
不用数了,7个f,记住前面的数字就是后面 f 的个数即可
其数值为