本文根据浙大翁伯的慕课C语言程序设计基础进行编写,里面添加了许多细节,用心品味,相信会得到一些帮助以及对C语言的新见解。
编译执行C程序
在linux上进行编译执行c程序:
gcc hello.c -o hello
,就会生成一个可执行文件,再输入./hello
,就会启动可执行文件。
注意事项
- C语言不支持嵌套函数定义
- 逗号运算符( , )是C语言运算符中优先级最低的一种运算符,结合顺序是从左至右,用来顺序求值(最后一个逗号后面表达式的值作为整个表达式的值)
c = (a>b, a+b); // 运行后c值为8,因为括号的优先级高于赋值运算符,所以先算括号内的表达式,此时计算结果为最后一个表达式的值,即a+b的值,所以c=8
C99变化
集成初始化
- 可以使用变量给数组赋予长度
- 数组赋值可以指定下标相应的,获得稀疏数组:`
a[10]={ [1]=1, 3, [5]=6};
//a = [0,1,3,0,0,6,0,0,0,0}
变量类型
size_t
size_t
类型是一个类型定义,通常将一些无符号的整形定义为size_t
,比如说unsigned int
或者unsigned long
,甚至unsigned long long
。每一个标准C实现应该选择足够大的无符号整形来代表该平台上最大可能出现的对象大小。
使用size_t
size_t
的定义在<stddef.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>
和<wchar.h>
这些标准C头文件中,也出现在相应的C++头文件, 等等中,你应该在你的头文件中至少包含一个这样的头文件在使用size_t之前。 包含以上任何C头文件(由C或C++编译的程序)表明将size_t
作为全局关键字。包含以上任何C++头文件(当你只能在C++中做某种操作时)表明将size_t作为std命名空间的成员。 根据定义,size_t
是sizeof
关键字(注:sizeof是关键字,并非运算符)运算结果的类型。所以,应当通过适当的方式声明n来完成赋值:
n = sizeof(thing);
考虑到可移植性和程序效率,n应该被申明为size_t
类型。类似的,下面的foo函数的参数也应当被申明为sizeof:
foo(sizeof(thing));
参数中带有size_t
的函数通常会含有局部变量用来对数组的大小或者索引进行计算,在这种情况下,size_t是个不错的选择。
适当地使用size_t
还会使你的代码变得如同自带文档。当你看到一个对象声明为size_t类型,你马上就知道它代表字节大小或数组索引,而不是错误代码或者是一个普通的算术值。
size_t()
// %lu 配合size_t输出
printf("%lu\n", );
数组、指针
数组长度
- 使用
sizeof()
int a[] = {1,2,3,4};
int len = sizeof(a)/sizeof(a[0]);
定义数组
在数组结束位置加一个逗号,方便再向数组后面添加元素。
int a[] = {0,1,2,};
const
数组
数组变量为const
类型指针,则表明数组的每个单元都是const int
,故必须对数组进行初始化赋值:
const int a[] = {1,2,3,4,5,};
数组与指针
在函数原型定义中,即函数的参数列表中,数组和指针其实是一样的东西:
// 以下四种函数原型是等价的
int sum(int *arr, int n);
int sum(int *, int);
int sum(int arr[], int n);
int sum(int [], int);
其实,数组变量本身就是表达地址,所以
int a[10];
int *p = a;
在这里无需使用&
取地址符。
但如果是要表达数组单元,因为数组单元是变量,则需要使用&
取地址符“
a == &a[0]; // 对数组的首元素取地址
解释数组不能相互直接赋值的原因
由于数组变量是const
的指针,所以不能被赋值,即
int a[] <==> int * const a=...
常量与变量
总可以是把一个非const
的值转换为常量:
int a = 15;
const int b = a;
b = a + 1; // ERROR!
动态内存分配
如果输入数据时,先由用户输入个数,再输入,则可以动态分配内存:
实例:实现动态数组长度
// 使用malloc函数分配内存,需要导入标准库头文件
#include <stdlib.h>
int *a = (int*)malloc(n*sizeof(int)); // a 为数组
free(a); // 使用完后需要释放分配的动态内存
对于free
,只有申请过的空间才需要释放。
解释:
malloc
函数返回的是void*
类型,需要转换为int*
类型。
运算符
取址符&
&
:获得变量的地址,操作数必须为变量。
例如:
// 使用%p格式化输出,可以获得变量的十六进制地址
int i; printf("%p\n", &i);
注意在不同架构系统下,地址变量的长度有所不同,在x32和x64架构下,整型
int
都是4位,而在x32下,地址长度也为4位,而在x64下,地址长度为8位。
指针
指针类型说明
int* p,q;
int *p,q
// 以上两种声明方式结果相同,都意味着声明一个指针变量p,和普通整型变量q
// int* p 和 int *p作用相同,不要搞混了
函数的指针参数
void f(int *p)
// code
int i = 0;
// 传参
int f(&i)
左值
在赋值号左边的不是变量名,而是值,是表达式计算的结果,如:
a[0] = 2;
*p =3;
指针与const
有两种情况:
- 指针本身为const\
- 指针所指向的值为const
指针是const
一个指针表示一旦得到某个变量的地址,就不能再指向其他变量。
// q指针变量本身就是const
int * const q = &i; //q 是 const
/ q 可以赋值,但不能进行修改运算
*q = 26; // ok
q++; // ERROR q表示指针变量本身
指向为const
表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)。
const int *p = &i;
*p = 26; // ERROR *p是const *p表示指针所指向的值
i = 26; // OK
p = &j; // OK
这里还有一种情况,
int const * p = &i;
,当const
再*
前,则代表着指针所指向的值为常量。
指针运用场景
交换两个变量的值
void swap(int *pa, int *pb)
{
int t = *pa;
*pa = *pb;
*pb = t;
}
作为参数,做保存返回结果的变量
函数需要返回多个值,某些值就只能通过指针返回。
void minmax(int a[], int len, int *max, int *min){
/// code
}
minmax(a, sizeof(a)/sizeof(a[0]), &max, &min)
函数返回运算状态,结果由指针返回
操纵成功返回1, 操作不成功返回-1
易错点
任何指针变量在没有得到一个实际变量地址之前,都不能对其进行赋值,可能使得指针变量指向一个奇怪的地址。
字符串
字符串定义
字符串数组==> 字符串:
# 通过在字符数组后加上一个元素'\0',来表示字符串
char word[] = {'H','I','\0'}'
0标志着字符串的结束,但它并不是字符串的一部分,所以在计算字符串长度时,不包含这个0元素。
注意: 字符串使用" "
来标记,单个字符使用' '
来标记,否则会出现Warning.
字符数组
定义字符串指针:
char *s = "Hello World";
注意:对于字符数组,如果定义的两个字符数组的值相同,则两个字符数组变量指向的地址为同一个地址,且这个地址非常小,被称作程序的代码段,而且是只读的,不能进行编辑,也就是说字符的内容不能被修改。
char *s = "Hello World";
char *s1 = "Hello World";
// s 和 s1 指向同一个地址,此时对s进行修改就是对s1进行修改
s[0] = 'B'; // 会报错
// 如果希望能修改字符的内容,则定义一个字符数组
char s[] = "Hello World!;"
s[0] = 'B'; // OK
Warning: 对于字符串数组而言,如果我们定义时规定了其长度,实际长度是规定长度-1:
// 规定string数组的长度为8,然而实际只能存储7个字符,因为最后一个要给 \0
char string[8];
字符串数组
可以使用二维数组来定义:
// 规定字符串数组内的每个元素长度为10以内
char string[][10]
字符串连接
当一行写不下字符串时,可以使用"
进行拼接:
printf("12345"
"6789");
// 输出为 123456789
// 也可以使用 \ 来拼接
printf("12345\
6789");
/* 注意在\后面的元素都会被输出,
以如果不希望出现tab缩进的空格时
就把tab空格删除*/
字符串输出输入
单字符输入输出
1. putchar
: 输入单字符
// 向标准输出写一个字符
int putchar(int c);
其返回值为写了几个字符,如果返回EOF(-1)
,则表示写失败。
2. getchar
: 读入一个字符
// 从标准输入读一个字符
int getchar(void);
返回类型是int
是为了返回EOF
。
3. 实例:
int main(int argc, char const* argv[])
{
int ch;
/ getchar 输入
while ((ch = getchar()) != EOF){
putchar(ch); //输出
}
return 0;
}
返回EOF
的方法:
- 在linux上,使用
CTRL + D
- 在windows上,使用
CTRL + Z
CTRL+D是使得shell在输入字符后添加一个(-1),使得出现EOF.
warning:在实际执行程序的时候,可能会发现,当输入多个字符后按回车,输入的字符都能被输出。
因为在输入和输出设备之间有一层叫做shell
的程序,shell
作为中间介质,作为行编辑作用,用来填充缓冲区。
多字符输入输出
使用%s
:
// scanf读入一个单词(以空格、tab、回车为止)
scanf("%s",string);
使用
scanf
是有危险的,因为不知道要输入的多长的字符,这时需要给scanf
约束长度:
// 约束只能输入长度最多为7的字符
scanf("%7s", string);
// 这个时候是根据字符长度来进行停止,而不是根据空格
char string[8];
char string1[8];
scanf("%7s", string);
printf("%s -- %s --\n", string, string1);
// 输出 1234567 -- 8 --
char*
与 字符串
- 字符串可以表达为
char*
的形式 char*
不一定是字符串- 只有
char*
所指向的字符数组尾部有0
,则表示为字符串
- 只有
空字符串
char buffer[100] = "";
// 这是一个空字符串,buffer[0] == '\0'
char buffer[] = "";
// 这个字符数组的长度只有1 !!!
枚举
常量符号化
实例:将颜色与数字映射
#include <stdio.h>
const int red = 0;
const int green = 1;
int main(int argc, char const *argv[])
{
int color = -1;
char *colorName = NULL;
scanf("%d", &color); // 将color的值该改变为输入值
switch( color ){
case red: colorName = "red"; break;
case green: colorName = "green", break;
default: colorName = "unknown"; break;
}
printf(“%s\n", colorName);
return 0;
}
在该例中,使用了符号(常量)而不是具体数字来代表程序的数字。
对于讲究代码整洁的我们,当然不会满意上述定义多个符号常量的复杂代码,这时我们就引进了枚举。
使用枚举
枚举不是单独定义的const int 变量 =
的形式,如下定义:
enum COLOR {RED, GREEN};
可以发现,我们并没有为RED,GREEN常量赋值,我们只要心里知道red
对应0,green
对应1,如果后面还要添加常量,则以此类推:
enum 枚举类型的名字(自定义) {名字0,名字1,...,名字n};
这里枚举类型的名字我们可以随意取,因为在程序中,并不会使用到,我们真正使用的是后面定义的名字,即常量符号,它们的类型为int
,值从0到n。
相关解释
其实,我们定义enum,相当于自己定义了一个数据类型,此时我们可以调用该类型:
#include <stdio.h>
// 定义一个color类型的枚举
enum color {red, green};
void f(enum color c);
int main()
{
// code
}
void f(enum color c)
{
printf("%d\n", c);
}
注意 : 在
color
类型前需要表明enum
。
枚举类型可以跟上enum作为类型,而实际上是以整数来做内部计算和外部输入输出的。
在实际过程中,很(bu)少(hao)用枚举类型。
结构
声明结构类型
// 声明一个日期结构
struct date {
int month;
int day;
int year;
}; // 注意结尾有 ; 号
struct date today;
today.month =11;
today.day = 31;
today.year = 2018;
学过面向对象的童鞋不难发现,这里结构体的作用与面向对象里的类定义很像,定义类属性,声明一个类的对象,然后对类对象进行属性赋值。
声明结构的形式
1.
struct point {
int x;
int y;
};
struct point p1,p2;
// p1, p2都是point结构,里面含有x和y的值
2.
struct {
int x;
int y;
} p1, p2;
// p1, p2都是一种无名结构,里面有x和y
3.
struct point {
int x;
int y;
} p1, p2;
// p1,p2都是point结构,里面含有x和y的值
对于第一种和第三种定义形式,都声明了结构point
,而第二种只是定义了两个变量。
结构初始化
#include <stdio.h>
struct date {
int month;
int day;
int year;
};
int main(int argc, const char *argv[])
{
// 第一种,安装结构体内变量,依次初始化
struct date today = {11,31,2018};
// 第二种,选择属性变量来赋值, 使用 .属性名 = 方式
struct date this_month = {.month=11, .year=2018};
}
结构运算
对于整个结构体,可以进行赋值、取地址,也可以传递给函数参数
p1 = (struct point){1,2}; // 相当于 p1.x=1, p1.y=2;
p1 = p2; // 相当于p1.x=p2.x, p1.y=p2.y;
结构指针
与数组不同的是,结构变量的名字并不是结构变量的地址,必须使用&
运算符:
struct date *pDate = &today;
函数
程序参数
在定义程序参数时,其实真正的定义如下:
int main(int argc, char const* argv[])
argv[0]
代表着命令本身
argv
数组可以接受在命令行下输入的参数
gcc test.c -0 test
./test
// 返回 ./test
./test 1 2
// 这个时候,我们在命令行下为程序输入了两个字符串参数 1 2
字符串函数
首先要导入头文件:#include <srting.h>
,如果不导入头文件,可能会引发implicit declaration of function ‘strncmp’
的类似报错。
1. strlen(const char* s)
: 返回s的字符串长度(不包括结尾的\0
)
2. strcmp(const char* s1, const char* s2)
:比较两个字符串,返回0、1、-1
分别对应 s1==s2 ; s1>s2 ; s1<s2。
对于strcmp
,是使用ASCII码值进行对比,来判断大小。
可以使用数组遍历下标比较,也可以使用指针来比较`
while(*s1 =? *s2)
s1++
s2++
3. char* strcpy( char *restrict dst, const char* restrict src)
: 将src的字符串拷贝给dst
restrict 表示src和dst不重叠,返回dst。
有时候src和dst的地址如果进行复制,则会重叠,为了避免这种情节,可以使用malloc
为dst动态分配一个地址:
// 注意赋予的地址大小为src长度+1
char* dst = (char*)malloc(strlen(src)+1);
strcpy(dst, src);
// 记得最后释放
free(dst);
char* mycpy(char* dst, const char* src)
{
int idx = 0;
while(src[idx]!='\0') {
dst[idx] = src[idx];
idx++;
}
dst[idx] = '\0';
return 0;
}
// 第二版本
char* mycpy(char* dst, const char* src)
{
char* ret = dst;
while( *dst++ = *src++ )
;
*dst = '\0';
return ret;
}
4. 把s2拷贝到s1的后面,结成一个长的字符串
char* strcat(char * restrict s1, const char* restrict s2)
返回S1,s1的空间必须足够。
注意: 使用strcpy
和strcat
是不安全的,因为无法判断复制字符串和连接字符串后,目的地址的空间是否足够。
安全版本的复制、连接、比较字符串
// 注意在函数和参数列表中多加了一个n
char* strncpy(char* restrict dst, const char* restrict src, size_t n);
char* strncat(char* restrict s1, const char* restrict s2, size_t n);
// 这里添加n,可以比较字符串前n位是否相等
int strncmp(const char* s1, const char* s2, size_t n);
n代表着最多能拷贝、连接的字符串长度。
实例:
#include <stdio.h>
#include <string.h>
int main(){
const char* s1 = "abc";
const char* s2 = "edf";
int a = strncmp(s1, s2, 2);
printf("%d\n",a );
return 0;
}
5. 字符串中寻找子字符
char* strchr(const char*s,int c);
如果函数返回为NULL,则代表没有找到子字符,如果找到了该字符,则返回字符串第一个出现该子字符的位置及其后面的全部字符。
实例:
// 返回第二个子字符
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char s[] = "hello";
// 寻找第一个'l'子字符
char *p = strchr(s, 'l'); // 返回 llo
// 寻找第二个'l'子字符,p+1即从返回值的下一个位置开始寻找
// 即从 lo 开始寻找
p = strchr(p+1, 'l');
printf("%s\n", p); // 返回lo
return 0;
}
另外两种寻找子字符串函数如下:
// 寻找子字符串
char * strstr(const char* s1, const char* s2);
// 不区分大小写的寻找子字符串
char * strcasestr(const char* s1, const char* s2);
注意:在使用strcasestr
函数时,必须在头部进行宏定义:
#define _GNU_SOURCE
#include <string.h>
输入输出类型
%p
表示输出这个指针,%d
表示后面的输出类型为有符号的10进制整形%u
表示无符号10进制整型,%lu
表示输出无符号长整型整数 (long unsigned
)
算法
判断一个数是否为素数
int isPrime(int x)
{
int ret =1;
int i;
// 如果x为1或2,或者x是否为不等于2的偶数
if( x==1 || ( x%2 == 0 && x!=2)
ret = 0;
// x大于2
for ( i=3; i<sqrt(x); i+=2 ) {
if ( x%i==0) {
// x不为质数
ret = 0;
break;
}
}
return ret;
}
易错点
数组
- 数组本身不能直接被赋值,只能采用遍历赋值。