1.常用字符串处理函数
(1)strcmp()函数
int strcmp(const char *cs, const char *ct)
{
unsigned char c1, c2;
while (1) {
c1 = *cs++;
c2 = *ct++;
if (c1 != c2)
return c1 < c2 ? -1 : 1;
if (!c1)
break;
}
return 0;
}
补:Memcpy,注意分类讨论,避免地址重叠!!
(2)strncmp()函数
int strncmp(const char *cs, const char *ct, size_t count)
{
unsigned char c1, c2;
while (count) {
c1 = *cs++;
c2 = *ct++;
if (c1 != c2)
return c1 < c2 ? -1 : 1;
if (!c1)
break;
count--;
}
return 0;
}
(3)strcpy()函数
char *strcpy(char *dest, const char *src)
{
if((dest == NULL)||(src == NULL))
{
return NULL;
}
char *tmp = dest;
while ((*dest++ = *src++) != '\0')
return tmp;
} 注意:为什么返回char * 类型,因为为了能使用链式表达式,同时做多个操作,如:getstrlen(strcpy(dest,src));
(4)strncpy()函数
char *strncpy(char *dest, const char *src, size_t count)
{
char *tmp = dest;
while (count) {
if ((*tmp = *src) != 0)
src++;
tmp++;
count--;
}
return dest;
}
(5)strcat()函数—src复制在dest后
char *strcat(char *dest, const char *src)
{
char *tmp = dest;
while (*dest)
dest++;
while ((*dest++ = *src++) != '\0') ;
return tmp;
}
(6)strncat()函数
char *strncat(char *dest, const char *src, size_t count)
{
char *tmp = dest;
if (count) {
while (*dest)
dest++;
while ((*dest++ = *src++) != 0) {
if (--count == 0) {
*dest = '\0';
break;
}
}
}
return tmp;
}
(7)atoi函数
int myAtoi(char * str)
{
int value = 0;
while( *str >= '0' && *str <= '9' )
{
value *= 10; //相当于十进制左移一位
value += *str - '0'; //将字符转换成int型存放到value最低位
str++; //指针偏移到下一个位置
}
if( *str != '\0' ) //字符串中含有非'0'~'9'的字符,返回失败
{ //str指向不是0-9的数,并且不是结束符’\0’,直接返回-1
return -1;
}
return value;
}
(8)字符串转正数 ——重点看判断溢出!!
int myAtoi(char* str) {
long ret = 0;
int flag = 1;//默认正数
//去除空格及判断符号位
for (; *str == ' '; str++);
switch (*str) {
case '-':
flag = -1;
case '+':
str++;
}
//排除非数字的情况
if (*str < '0' || *str > '9') {
return 0;}
while (*str >= '0' && *str <= '9') {
ret = ret * 10 + (*str - '0');
//判断溢出
if ((int)ret != ret) {
return (flag == 1) ? (INT_MAX) : (INT_MIN);
}
str++;
}
ret *= flag;
return (int)ret;
}
(9)斐波那列数列
int F(int n)
{
if(n==1 || n==2)
return 1;
else
return F(n-1)+F(n-2);
}
(10)正数转字符串 (注意把整数各位上的数字加’0’转成char型并存在字符数组中,注意字符串逆序和判断正负!)
void itoA(int n, char *str)
{
char buf[10] = “”;
int i = 0 ,len = 0;
int temp = n < 0 ? -n : n ;
if (str == NULL)
return;
while (temp)
{
buf[i++] = (temp % 10) + ‘0’;
temp = temp / 10;
}
len = n < 0? ++i : i ;
str[i] = 0;
while(1)
{
i–;
if(buf[len-i-1] == 0)
{
break;
}
str[i] = buf[len-i-1];
}
if(i == 0)
{
str[i] = ‘-’;
}
}
2.堆和栈的区别是?
1、堆栈空间分配
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式类似链表。
2、堆栈缓存方式
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。
堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度慢一些。
3、效率比较
栈由系统自动分配,速度较快,但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
4、存储内容
栈: 一种先进后出的数据结构。在函数调用时,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向函数的返回地址,也就是主函数中的下一条指令的地址,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
3.ARM里的大端格式和小端格式分别是什么意思?
大端模式,是指数据的高位,保存在内存的低地址中,而数据的低位,保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
小端模式,是指数据的高位保存在内存的高地址中,而数据的低位保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
举例:int a = 0x12345678;
a是四字节的int类型变量,需要占四个字节空间,假设变量a的首地址是0x2000,那么数据存储在地址中 的格式如下:
0x2000 0x2001 0x2002 0x2003 地址
0x12 0x34 0x56 0x78 大端模式存储
0x78 0x56 0x34 0x12 小端模式存储
——————————————————————————————————————————————
用c语言判断机器是大端或小段模式:
(1)union联合体方法:
(2)指针方法:
分析过程如下:
4.ioctl是如何从用户态到内核态的(保留问题,待解决)
5. 一次访问http的协议的完整过程(保留问题,待解决)
6. const四种情况分析
const修饰指针,一般分为如下四种情况:
int b = 500;
const int a = &b; //情况1
int const a = &b // 2
int const a = &b // 3
const int const a = &b// 4
如何区别呢?
如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
7.sizeof 和strlen![](https://i-blog.csdnimg.cn/blog_migrate/b2663ac8f111eb73abad99b05b1ec6f2.png)
p表示一个指针,大小始终是4字节,*p表示的是p指向变量的类型,是char型,所以是1
数组作为函数传参时,类似于一个指针的作用,所以结果是4
8 static
(1)static关键字在C语言中有2种用法,而且这两种用法彼此没有任何关联、完全是独立的。其实当年本应该多发明一个关键字,但是C语言的作者觉得关键字太多不好,于是给static增加了一种用法,导致static一个关键字竟然有两种截然不同的含义。
(2)static的第一种用法是:用来修饰局部变量,形成静态局部变量。要搞清楚静态局部变量和非静态局部变量的区别。本质区别是存储类不同(存储类不同就衍生出很多不同):非静态局部变量分配在栈上,而静态局部变量分配在数据段/bss段上。
(3)static的第二种用法是:用来修饰全局变量,形成静态全局变量。要搞清楚静态全局变量和非静态全局变量的区别。区别是在链接属性上不同
①普通的(非静态)的函数/全局变量,默认的链接属性是外部的
②static(静态)的函数/全局变量,链接属性是内部链接。
分析:
1、静态局部变量在存储类方面和全局变量一样。
2、静态局部变量在生命周期方面和全局变量一样。
3、静态局部变量和全局变量的区别是:作用域、连接属性。静态局部变量作用域是代码块作用域(和普通局部变量是一样的)、链接属性是无连接;全局变量作用域是文件作用域(和函数是一样的)、链接属性方面是外连接。
9 typdef和#define宏区别
(1)原理不同
#define是C语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。
typedef是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已经存在的类型一个别名,但不能在一个函数定义里面使用typedef。用typedef定义数组、指针、结构等类型会带来很大的方便,不仅使程序书写简单,也使意义明确,增强可读性。
(2)功能不同
typedef用来定义类型的别名,起到类型易于记忆的功能。另一个功能是定义机器无关的类型。如定义一个REAL的浮点类型,在目标机器上它可以获得最高的精度:typedef long double REAL, 在不支持long double的机器上,看起来是这样的,typedef double REAL,在不支持double的机器上,是这样的,typedef float REAL
#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
(3)作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。
(4)对指针的操作不同
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1, p2;
INTPTR2 p3, p4;
含义分别为,
声明一个指针变量p1和一个整型变量p2
声明两个指针变量p3、p4
#define INTPTR1 int*
typedef int* INTPTR2;
int a = 1;
int b = 2;
int c = 3;
const INTPTR1 p1 = &a;
const INTPTR2 p2 = &b;
INTPTR2 const p3 = &c;
const INTPTR1 p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是p1可以指向其他内容。
const INTPTR2 p2是一个指针常量,不可使p2再指向其他内容。因为INTPTR2表示一个指针类型,因此用const限定,表示封锁了这个指针类型。
10.结构体字节对齐
对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合 :按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。
比如有如下一个结构体:
struct stu{
char sex;
int length;
char name[10];
};
struct stu my_stu;
由于在x86下,GCC默认按4字节对齐,它会在sex后面跟name后面分别填充三个和两个字节使length和整个结构体对齐。于是我们sizeof(my_stu)会得到长度为20,而不是15
——————————————————————————————————————————————
我们可以按照自己设定的对齐大小来编译程序,GNU使用__attribute__选项来设置,比如我们想让刚才的结构按一字节对齐,我们可以这样定义结构体
struct stu{
char sex;
int length;
char name[10];
}attribute ((aligned (1)));
struct stu my_stu;
则sizeof(my_stu)可以得到大小为15。
上面的定义等同于
struct stu{
char sex;
int length;
char name[10];
}attribute ((packed));
struct stu my_stu;
attribute((packed))得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐.
11.offsetof宏和container_of宏
(1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。
(2)offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。
——————————————————————————————————————————————
(1)container_of宏作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
(2)typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。
(3)container_of宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。
12.c语言源码到可执行程序过程
源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。
(1)编程中常见的预处理
①#include(#include <>和#include ""的区别)
②注释
③#if #elif #endif #ifdef
④宏定义
注意:第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义);第二,typedef重命名语言还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理器处理的);
13.宏定义常考类型
(1)MAX宏,求2个数中较大的一个
#define MAX(a, b) (((a)>(b)) ? (a) : (b))
(2)SEC_PER_YEAR,用宏定义表示一年中有多少秒
#define SEC_PER_YEAR (3652460*60UL)
14.volatile
(1)volatile的字面意思:可变的、易变的。C语言中volatile用来修饰一个变量,表示这个变量可以被编译器之外的东西改变。编译器之内的意思是变量的值的改变是代码的作用,编译器之外的改变就是这个改变不是代码造成的,或者不是当前代码造成的,编译器在编译当前代码时无法预知。譬如在中断处理程序isr中更改了这个变量的值,譬如多线程中在别的线程更改了这个变量的值,譬如硬件自动更改了这个变量的值(一般这个变量是一个寄存器的值)
(2)以上说的三种情况(中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量)都是编译器在编译时无法预知的更改,此时应用使用volatile告诉编译器这个变量属于这种(可变的、易变的)情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。
(3)编译器的优化在一般情况下非常好,可以帮助提升程序效率。但是在特殊情况(volatile)下,变量会被编译器想象之外的力量所改变,此时如果编译器没有意识到而去优化则就会造成优化错误,优化错误就会带来执行时错误。而且这种错误很难被发现。
(4)volatile是程序员意识到需要volatile然后在定义变量时加上volatile,如果你遇到了应该加volatile的情况而没有加程序可能会被错误的优化。如果在不应该加volatile而加了的情况程序不会出错只是会降低效率。所以我们对于volatile的态度应该是:正确区分,该加的时候加不该加的时候不加,如果不能确定该不该加为了保险起见就加上。
15.单链表
1、创建新节点
typedef struct node
{
int data;
node * next;
}node;
2、遍历
3、插入节点
4、删除节点
5、倒叙
6、查找链表中倒数第K个节点
struct ListNode{
int value;
ListNode* next;
};
ListNode* findKthToTail(ListNode *phead,unsigned int k)
{
if (k == 0)//(k < 0) //考虑到K小于等于0时的情况
return nullptr;
if (phead == nullptr)
return nullptr;
ListNode *p1 = phead;
ListNode *p2 = phead;
for (int i = 1; i < k; ++i)
{
if (p1->next == nullptr){//若k比本身链表的最大长度还大则直接返回nullptr
return nullptr;
}
p1 = p1->next;
}
while (p1->next != nullptr)
{
p1 = p1->next;
p2 = p2 ->next;
}
return p2;
}
16.双链表
1、创建链表
注:(忘记清空申请内存了)
2、插入
————————————————————————————————————————————
头插入注意程序语句顺序!!!
3、遍历
(1)双链表后项遍历和单链表一样
(2)前项遍历
4、双链表删除节点
17.argc、argv与main函数的传参
1、谁给main函数传参
(1)调用main函数所在的程序的它的父进程给main函数传参,并且接收main的返回值。
2、为什么需要给main函数传参
(1)首先,main函数不传参是可以的,也就是说父进程调用子程序并且给子程序传参不是必须的。 int main(void)这种形式就表示我们认为不必要给main传参。
(2)有时候我们希望程序有一种灵活性,所以选择在执行程序时通过传参来控制程序中的运行,达到不需要重新编译程序就可以改变程序运行结果的效果。
3、表面上:给main传参是怎样实现的?
(1)给main传参通过argc和argv这两个C语言预订的参数来实现
(2)argc是int类型,表示运行程序的时候给main函数传递了几个参数;argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。argv[0]就是我们给main函数的第一个传参,argv[1]就是传给main的第二个参数····
4、本质上:给main传参是怎样实现的?
(1)上节课讲过,程序调用有各种方法但是本质上都是父进程fork一个子进程,然后子进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
(2)程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的。
5、给main传参要注意什么
(1)main函数传参都是通过字符串传进去的。
(2)程序被调用时传参,各个参数之间是通过空格来间隔的。
(3)在程序内部如果要使用argv,那么一定要先检验argc。