自己实现sprintf功能:
关于C中的系统函数sprintf在上次【https://www.cnblogs.com/webor2006/p/7545627.html】学习中已经用到过了,这里再来回顾一下:
![](https://i-blog.csdnimg.cn/blog_migrate/38e881fa79d8fecc0c9aadf9d4a16f5e.png)
而为了巩固学习咱们可以用之前所学的可变参数【https://www.cnblogs.com/webor2006/p/9499213.html】、指针等知识实现类似的功能,自己实现一个只考虑传整型参数的情况就成,那如何来实现呢?下面开始:
![](https://i-blog.csdnimg.cn/blog_migrate/fdf712a08b813777e2f1b092d8cc7372.png)
![](https://i-blog.csdnimg.cn/blog_migrate/49b35a227cffb8e02f6aff9e31865b40.png)
如果遇到了“%”,则需要判断一下它的下一位字符是否是“d”字符,只有这样才是一个合法的占位,所以:
![](https://i-blog.csdnimg.cn/blog_migrate/ae944a96d465f8d713edffc04d06b7d1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8f07b786badb3210c88cd0af2e88070a.png)
然后如果发现此参数是一个负数,则需要前面手动加一个“-”,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/4f4fe5b9cdcec0f7800aaffa6d5efde2.png)
然后再将解析到的字符串参数遍历到结果串当中,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/0e52d5b1262d5a44c1083104564f7b2c.png)
下面使用一下咱们自己编写的函数看下效果:
![](https://i-blog.csdnimg.cn/blog_migrate/881af2cbc8121ab0d8fd7d7565f1ae12.png)
原来是少了这么一句关键逻辑,如下:
//
// Created by xiongwei on 2018/9/23.
//
#ifndef LSN3_EXAMPLE_MYSPRINTF_H
#define LSN3_EXAMPLE_MYSPRINTF_H
#include <stdarg.h>//用来获取可变参数
void mysprintf(char *buffer, const char *fmt, ...) {
//首先声明va_list
va_list arg_list;
va_start(arg_list, buffer);
char *b = buffer;
int count = 0;//用来记录总格式化字符的总个数,因为需要给结果字串最后位置添加一个'\0'
while (*fmt)//一个个格式字串字符进行遍历判断,如果字符串遍历完,其整个逻辑也就处理完了
{
if (*fmt != '%') {//如果格式字符中木有遇到"%"的占位符,则将相应的字节拷贝到buffer当中
count++;
*b++ = *fmt++;
continue;
}
fmt++;
switch (*fmt) {
case 'd': {
int i = va_arg(arg_list, int);//获得一个可变参数
int j = 0;
char tmp[10];//将可变参数一个个字节存放在此临时变量中
int sign = i < 0 ? 1 :0;
do {
//i = 888
//取出最后一个数字
int r = i % 10;
r = r < 0 ? -r : r;
//去掉最后一个数字
//将其数值转换成字符记录一下
tmp[j++] = r + '0';
} while (i /= 10);
//tmp = 888
// i= -123 tmp = 321-
if (sign) {//负数参数处理
tmp[j++] = '-';
}
while (j>0)
{
char a = tmp[--j];
*b++ = a;
count++;
}
}
break;
}
fmt++;
}
buffer[count] = '\0';//在最后结果字符中增加一个字符串结束标记
}
#endif //LSN3_EXAMPLE_MYSPRINTF_H
此时再编译运行:
![](https://i-blog.csdnimg.cn/blog_migrate/db7a21f3a7f7fd0e58501099bc8e4f3b.png)
结构体:结构体是C编程中一种用户自定义的数据类型,类似于Java的JavaBean:
下面来定义一个结构体:
![](https://i-blog.csdnimg.cn/blog_migrate/4d0731dc6a15e40b09a564781cf7dbc8.png)
其中注意:结构体中的所有变量都是public的,下面来使用一下:
![](https://i-blog.csdnimg.cn/blog_migrate/28e911e05ce3b682c1eb15a76ac5b43c.png)
也可以在定义struct时就指出变量,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7c6c5be1bb9fa61eeb849fbab2779973.png)
另外还有一个定义方法,采用宏,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/a9fb5a2ae3a4b75278a073baa258f541.png)
- 字节对齐默认对齐方式:
先来问一下对于上面这个结构体,它占多少字节呢,咱们可以用sizeof来打印一下:int不是占4个字节、short占2个字节,加起怎么的也不可能是8个字节呀,下面再来定义两个结构体查看一下它的字节大小:
应该是2+2+4=8个字节嘛,为啥第二个还等于12个字节呢?这里也就是对于c中结构体还存在一个字节对齐的概念,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址开始访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。字节对齐的问题主要就是针对结构体。
如果没有手动指定字节对齐方式,就会按自然对齐进行字节对齐,那自然对齐是按什么规则对齐的呢?如下:
1、某个变量存放的起始位置相对于结构的起始位置的偏移量是该变量字节数的整数倍。
如何理解,咱们结合代码来理解一下:
先来看MyStruct1,它占的字节长度为8,咱们可以把里面元素的地址给打印出来便于分析:结合这个理论来理解一下,对于MyStuct1这个结构体的起始位置也就是它里面第一个元素i的起始位置为:
而对于第二个元素j,是一个short类型,占2个字节,那么其起始位置必须是2的整数倍,所以刚好是:
这两段内存刚好也是一个连续地址,接着第三个元素k,它是int占4个字节,所以起始位置必须是4的整数倍,地址往后增的话刚好是它:
所以从地址开始oxec4e1860到地址结束0xec4e1864+4【因为int占4个字节,所以结束地址肯定得基于起始地址增4个字节】,所以刚好是8个字节,不多不少。
接着再来分析MyStruct2,从打印中它是占12个字节,这个就有些不如预期,但是还是符合这个理论滴,下面来解释一下,还是一样先将它里面的元素的起始地址打印一下:可见MyStruct2的内存地址是从0xee4fd850开始的,也就是第一个元素i的起始地址,占据2个字节,接着第二个元素是一个int,占据4个字节,照理该变量的起始地址是紧接着之前的是从0xee4fd852开始,但是由于最后一位2很显然不是int的总长度4字节的整数倍,所以起始地址会对齐为0xee4fd854,而中间会空缺两个无效的地址:0xee4fd852、0xee4fd853,这就是所谓的内存对齐。接着到第三个变量k了,它占据2个字节,由于int占4个字节,所以它的结束地址为:0xee4fd857,往后一个地址为0xee4fd858应该为k的起始地址,由于该起始刚好是2的整数倍,所以整个结构体的长度为:short2字节+内存对齐空缺的2字节+int4字节+short2字节=10,呃~~但是我们看到的结果MyStruct2是占12个字节呢,这又是为何呢?这就得看下面另外一个规则了。
2、结构所占用的总字节数是结构中字节数最长的变量的字节数的整数倍。
回到上面提出的疑问,我们按第一条规则计算怎么也不可能是12个字节,加上这条规则就可以解释啦,由于MyStruct2中变量中最长的是int,也就是占4个字节,所以整个结构体的总字符数需要对齐到最长变量字节数的整数倍,很显然10不是4的倍数,所以得扩到12,这样才满足条件。由于有这条对齐规则存在,所以对于上面说的MyStruct2为啥最终长度是8就需要加上这个规则理解才算是完整的:因为整个变量的字节加起来就是8个,而它刚好是最大变量int的整数倍,所以也不存在扩容了,最终字节的长度就占8个字节。非默认对齐方式:
我们可以更改对齐行为,对于MyStrcut2我们可以指定以2个字节对齐,具体如何做呢?这是为啥?可以看到MyStruct2此时的内存是一个连接的空间,首先起始是从0xe3a5d858,然后占2个字节,接着到了第二个变量的起始地址就为0xe3a5d85a,由于目前设置的对齐字节数是2个字节而非4个字节了,所以刚好这个超始地址为2的整数倍,不用跳地址,接着再到第三个变量的起始地址就为0xe3a5d85e,由于它也是2的整数倍,所以也不用跳地址,刚好整个字节的长度就为:long2字节+int4字节+long2字节=8字节。【提示】:合理的利用字节可以有效地节省存储空间,不合理的则会浪费空间、降低效率甚至还会引发错误。(对于部分系统从奇地址访问int、short等数据会导致错误)。另外设置对齐的数字只能是2的倍数,不能是奇数,如下:
共用体:
定义:在相同的内存位置存储不同的数据类型,共用体占用的内存应足够存储共用体中最大的成员。
那下面来编写一个共用用:
![](https://i-blog.csdnimg.cn/blog_migrate/e4729922914fef84e0f4671d55a5c914.png)
但是~~如果咱们改变一下j的值,看结果会有啥变化:
![](https://i-blog.csdnimg.cn/blog_migrate/1e910a23bb55f8291374701d640ac67a.png)
为啥?咱们将i和j的地址打印一下就明白了:
![](https://i-blog.csdnimg.cn/blog_migrate/be6222fea4b87fd8538b895465c3f9f0.png)
所以当改了j之后,其i的值也被更改了。
那。。共用体有啥作用呢?其实也就是节省内存,比如说程序中要使用到这三个变量:
int i = 0;
int j = 0;
int k = 0;
然后同一时刻这三个变量只会用其中一个,那如果没有共用体的话,那每个变量都会占用4个字节,而如果将其定义到共用体当中,那只会占用4个字节的空间,相当于内存复用。
C++开端:
- 输出:
先新建一个新的cpp项目:首先的话题就是输出:C使用printf向终端输出信息,而C++提供了 标准输出流,比较简单,如下:
- 函数符号兼容:
我们知道对于C的大部分代码可以在C++中直接使用,但是仍然有需要注意的地方。
咱们先新建c文件,定义一个函数:
然后我们在cpp中来使用c中的函数:编译一切正常,来运行一下:
此时需要加一句这个来解决:
那"extern c"的作用是啥呢?为了说明这个问题,下面来做一个实验:
先在目录中建立一个main.c文件,如下:然后里面的代码为:
接着再生成一个cpp文件,里面的内容也是跟main.c一模一样:
接下来用gcc来分别编译一下,其中gcc环境安装可以参考:https://blog.csdn.net/sbvfhp/article/details/8075532:
接着咱们用nm命令来查看一下这两个生成的.o文件:接着用同样的方式来查看一下maincpp.o:
发现在c++当中的函数编译之后对其进行重命名了,其中test后面的ii也就是test(int x, int y)参数类型,而在C当中没有进行重命名,那这证明了一个问题:c和c++对于同一个函数编译出来的符号是不相同的,那么这样导致的问题就在于: c的.h头文件中定义了test函数,则.c源文件中实现这个函数符号都是test,然后拿到C++中使用,.h文件中的对应函数符号就被编译成另一种,和库中【也就是指c的该函数的实现】的符号不匹配,这样就无法正确调用到库中的实现。回到咱们的代码来理解:
所以就无法在cpp中来正常的调用到c定义的test()函数了,而加了extern之后,就是告诉编译器强制以c的形式进行编译,也就是不会对test()进行重命名了,所以就能够正常运行了,如下:
其更加好的做法应该是将extern写在.h头文件当中,利用宏来,如下:而需要注意的是由于头文件中已经有extern了,则在源代码中就不需要重复编写了,否则在有些编译上是会有问题的,如下:
应该将它去掉:
再次编译运行:
extern 关键字 可用于变量或者函数之前,表示真实定义在其他文件,编译器遇到此关键字就会去其他模块查找。 - 引用:
这是是C++定义的一种新类型,java也有,下面来看一下:如果来声明一个引用类型的函数,如下:
引用和指针是两个东西,引用 :变量名是附加在内存位置中的一个标签,可以设置第二个标签,简单来说 引用变量是一个别名,表示一个变量的另一个名字。
- 字符串:
①、C字符串:
字符串实际上是使用 NULL字符 `'\0' `终止的一维字符数组。表现形式如下:
②、字符串操作:
这里主要是对常用的一些字符串操作函数罗列一下,待实际使用时查找:函数 描述 strcpy(s1, s2); 复制字符串 s2 到字符串 s1。 strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。 strlen(s1); 返回字符串 s1 的长度。 strcmp(s1, s2); 如果 s1 和 s2 相同,则返回 0;如果 s1 < s2 则返回小于0;如果 s1>s2 则返回大于0,说明:两个字符串自左向右逐个字符相比(按ASCII值大小相比较) strchr(s1, ch); 返回指向字符串 s1 中字符 ch 的第一次出现的位置的指针。 strstr(s1, s2); 返回指向字符串 s1 中字符串 s2 的第一次出现的位置的指针。
③、C++ string类:
C++ 标准库提供了 string 类类型,定义在头文件string当中,支持上述所有的操作,另外还增加了其他更多的功能。有如下使用形式:
而在C++使用string可以进行如下操作:
其中如果是在堆中new的string,调用相应的方法就得用"->"来操作了,如下:
其中对于字符串指针在传输时效率比直接传字符串对象要高,所以可以根据实际需要动态选择。 - 命名空间:
namespace 命名空间 相当于java的package,下面来使用一下:那如果想要调用此test()方法,直接调用肯定是不行的,如下:
需要加域作用符:
而命名空间是可以嵌套的,如下:
而如果想在调用时不加域作用符就可以用using,类似于java中的import操作,如下:
而如果想完全不写域作用符,那还可以这样:
所以基于此,对于之前我们编写代码中使用了好多域作用符,如下:
简化的话就是使用using来包含std这个命名空间,如下:
对于域使用符还有另外一个作用,如下:
那如果想输出全局变量的ii该怎么慢,此时就可以使用域作用符,如下:
也就是当全局变量在局部函数中与其中某个变量重名,那么就可以用::来区分。