C/C++

数据类型

C语言中的基本数据类型,对于它分为两种:

1、signed 有符号的类型,也就是支持正负号的。

2、unsigned 无符号的类型,也就是没有负号,取值从0开始。

有符号和无符号的数据类型有啥区别呢?其实就是取值范围不一样,下面看一张对照表:

C中的基本整形数据类型为:int 、short、long、char。其中发现上面int 和 long在C中占的字节数是一样的,都是占4个字节,这个有别于java,在java中long是占8个字节嘛,下面可以用sizeof()来打印一下其类型的长度:

对于这个其实是随编译器而异的,下面来总结一下不同编译器下的基本数据类型所占的字节数:

16位编译器

char :1个字节
char*(即指针变量): 2个字节
short int : 2个字节
int:  2个字节
unsigned int : 2个字节
float:  4个字节
double:   8个字节
long:   4个字节
long long:  8个字节
unsigned long:  4个字节


32位编译器

char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int:  4个字节
unsigned int : 4个字节
float:  4个字节
double:   8个字节
long:   4个字节
long long:  8个字节
unsigned long:  4个字节


64位编译器

char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int:  4个字节
unsigned int : 4个字节
float:  4个字节
double:   8个字节
long:   8个字节
long long:  8个字节
unsigned long:  8个字节
复制代码

其实long int = long;在标准中规定int至少要和short一样长,long至少要和int一样长。

在实际中可能会用一个更加清晰的数据类型,如:

其实用的就是定义好的宏

这种写法是被推荐的,因为会比较清晰。

基数数据类型除了上面的整型之外,还有浮点型,具体如下表:

另外需要注意:在C中并没有专门的boolean类型,而是:非0既true、非null为true;

输出格式化

必须要写一个格式化占位符参数,其实跟java中的String.format()的用法类似,如:

而其中的“%d”表示输出整型变量,那对于其它数据类型其输出占位符又如何写呢,其它之前的表格中已经有说明,如下:

虽说"%d"可以输出所有的整型,但是还是用上图中对应的输出会更加精准。

另外sprintf()这个函数在实际当中也非常常用,比如要打印某个目录下的按规律生成的文件,比如:

也就是將2、3参数格式化的字符复制到str当中。

数组与内存布局

在C中声明数组必须指定长度,或者声明与赋值写在一起

另外它是在栈上分配内存的,而栈上的内存是有限制的,在mac上可以使用“ulimit -a”来查看其最大栈内存:

也就是最大栈的大小是8192K,但是需要注意:并不是我们程序也能申请这么大的栈内存的,因为像程序的一个函数参数,返回值等也是存放在栈中的。另外栈内存出了作用域就会自动释放掉,所以不需要手动去回收的。

前面说了栈大小不是特别大,那如果对于要的内存超过栈大小的该怎么办呢,当然就是在堆中进行申请喽,此时就存在以下几种堆中申请内存的一些函数,下面来说明下:

  • malloc:在堆中申请内存但不会对其申请的内存进行初始化,如在堆中申请1MB的内存:

另外还需要注意:由于申请的内存还没初始化,所以一般在malloc申请内存之后会使用memset保存其申请的内存是一片纯白的,而不是用了之前的脏数据,因为申请内存有可能会重用之前的内存,具体用法如下:

还有一点需要注意:堆中申请的内存是不会自动释放的,需要手动去释放,如下:

  • calloc():申请内存并将内存初始化为null,具体用法:

其实它就等价于:

  • realloc():重新对malloc申请的内存大小进行调整,如下:

那什么场景会用到它呢,这里举一个TCP传输粘包问题,比如发送“1,2,3,4,5,6”数据,而接收的时候可能分几次才能接收完,比如是先接收到了“1,2,3”,之后再接收到了“4,5”,最后接收了“6”,至此才将数据接收完,那此时的缓冲区char首先申请的是3个字节,于是乎“1、2、3”刚好接收满了,但此时还不是一个完整的数据包,所以还得接着等“4,5,6”,当接收到了“4、5”了,就需要对缓冲区进行扩容用以存放这两个字节了,同样的最后接收到了"6",则继续再要对缓存冲再扩容一个字节。 当然直接申请一个足够大的缓存区不就不用扩容了么,这是因为数据包的大小是无法确定的,这里只是为了说明问题举了个简单的粟子而已。

  • alloca():向栈中申请内存。用法如下:

内存布局

  • 物理内存:通过物理内存条获得的内存空间。
  • 虚拟内存:它是一种内存管理技术,能够均处一部分硬盘空间充当内存使用。

而在C当中的内存布局如下:

其中最顶部的是内核空间:

除这个内核空间之外的则是用户进程的内存空间:

下面看一下有哪些内容,首先是栈区:

接着是内存映射段:

接着就是堆区了:

接着就是BSS段了:

接着再就是数据段:

最后一个则是文本段:

咱们基于上面的来画一个简化版本:

其中“预留区”是程序看不见的区域,系统预留滴。

这里来对堆内存地址由低往高进行说明:在堆区申请内存是调用了glibc(C的标准库、运行库,类似于java的JDK)提供的malloc方法,而它的底层是由Linux的brk和mmap两种方式来实现的,而其中:brk申请内存的方式是将内存指针(假设为_edata)往高地址堆,目前_edata指向堆内存的起始位置 :

假如申请10K的内存,此时就会将_edata由低地址往上推10K的大小,如下:

如果再申请一个10K,同样的往上再推10K,如下:

那如果A被释放掉了,会发生什么情况呢?此时的_edata并不会回退,而是A这个10K的区域成了内存碎片了,如下:

那此时如果再申请一个10K的内存,发现A这个空间刚好满足则会重用它,_edata并不会往上再去开辟新内存空间,那假如申请的内存大于10K,比如11K,此时A这个区域内存满足不了要申请的11K大小,所以还是会往上推11K大小的内存,如下:

那brk方式申请的内存就永远不会收缩么,其实不是这样的,像这种场景就会:此时C被释放了,内存就会收缩了,如下:

而对于mmap申请内存的方式为:找一块满足大小的内存既可,而不会像brk方式往上今次推指针,所以它的内存随时都可以被释放的,那什么时候用brk,什么时候用mmap呢?其实是要申请的堆内存小于128k则用brk方式申请,否则用mmap申请,注意:此128K是个阈值,是可以人为配置的。

好,明白了上面的之后,回到咱们开篇所指出的问题:为啥在malloc动态申请内存之后,需要用memset手动再去给内存进行一个初始化?因为brk方式有可能会存在复用之前申请过的内存,如果不初始化有可能该内存是之前申请过的,这样就会造成一些数据的混乱。

那对于malloc底层为啥不全采用mmap方式来实现呢?因为mmap效率明显不如brk推指针的方式,所以就存在于两种方式来实现了。

另外对于数组而言其实是一段连续的内存地址,如下:

头文件基础知识

我们知道对于C、C++的代码通常是有.h头文件和.c&.cpp的源文件的,如下:

那么在.h头文件中能否有具体实现呢?答案是肯定的,下面来试验一下:

另外对于要使用指定头文件是需要用include来将其包含进来的,类似于java中的import,如下:

但是!跟java中的import是有区别的,在java中是不能够传递import的,怎么理解,看下面java代码:

而ArrayList里面是import了它了:

那如果我们在main中也想用Consumer这个类的话,还需要再导一遍,如下:

也就是说:虽然ArrayList已经import过了Consumer,而我们在main中也已经import了ArrayList,但是Consumer并不会被传递到main方法中,使用时是需要再次导入的,但是!C中是可以传递include的,下面用代码来说明一下:

然后在main.h中去include我们新建的这个头文件:

那我们在main.c中能否去调用a头文件中声明的test3()函数呢,当然能:

那思考一下为啥C、C++要分一个头文件和源文件,而不像Java只有一个源文件呢?其实.h就是将行为给暴露,其具体实现不暴露,当然如果想暴露具体实现那可以在.h中去用具体的方法来暴露,如:

而通常的只定义了函数的声明,如:

这样当别人想使用该函数时只需要include头文件既可,具体的实现细节则不会暴露给调用者。

指针

“指针是一个变量,它的值是一个地址。”,其中指针变量的声明有如下三种形式:

其中第一种是被推荐的写法。

其中还需要注意:在声明指针时如果未赋值,则是一个野指针【也就是有可能指向了一个不能被使用的地址从而造成程序的错误】,所以在声明时一定要赋值,如下:

那如果想取变量的地址则可以用“&”符,如下:

那如果想获取指针指向变量地址的值则需要用“*”解引用的操作,如下:

下面来看一下p指针占用了几个字节:

需要注意的是:由于目前是在64位系统上运行的,所以是8个字节,如果是在32位运行则长度是4个字。

有了指针之后就可以用它去操纵内存,下面来通过指针的形式来修改变量的值,如下:

指针是可以进行++、--操作的,比如用指针来遍历数组,下面来看下:

其中“array_p1++”是先取了值,然后再对其指针进行++,如果是写成"++array_p1",则是先对指针进行加加,然后再取值,最终输出就会漏掉一个,如下:

其中还有一种直接通过数组来进行相加也能达到遍历的目的,如下:

要取其数组的内容则需要解引用:

另外还有一个细节:为啥数组取地址时木有加“&”符号:

这是因为在C中数组名就是数组的首地址,下面来看下:

下面有个概念需要弄清楚:“数组指针”和“指针数组”,这个在面试可能会经常变问到,下面来看下:

其中指向的数组的元素个数为3,如果咱们想要通过数组指针array_p2来获得第二维的55,如何来写呢?

首先肯定得要将数组的指针+1,来定位到第二维的数组,所以array_p2+1,然后再取出它的值则是*(array_p2+1),接着这个值是一个数组,所以还得数组名+1来将指针移到要输出的第二个元素上来,所以此时为*(array_p2+1)+1,最后再解引用取出指针的值,所以整个的式子如:((array_p2+1)+1),下面来验证一下:

接下来更绕的来了,先把代码写出来:

先记着这个原则:“从右往左看 const 修饰谁 谁就不可变”:

意味着不能通过p2来修改tem的值,如下:

因为const是修饰的char,而非p2变量,所以p2的内容可以被更改,如下:

继续来理解下一个:

这个跟上一个效果是一模一样的,为啥?因为const只能修饰char,不能修饰*。

继续看下一个:

还是按照从右往左的原则,const这次修饰的是变量p4,也就是说p4的内容是不允许修改的,如下:

但是可以通过指针修改指向地址的值,如下:

下面两个是啥都不能变了,如下:

拿p5举例,既不能修改p5指针的值,如下:

下面再来看一个跟指针相关的东东---多级指针:

解引用则为:

函数

函数声明

C中的函数跟Java的方法基本类似,但是在C中的函数需要注意:我们使用的函数必须在之前声明,否则会编译不过,如下:

可以在之前做一个声明既可:

所以一般函数都声明在头文件中,然后一.c文件中头部进行include,这样就如同上面的声明一样了。

函数传参

  • 传值:把参数的值给函数,如下:

也就是说不会改变原有变量的值。

  • 传引用:

也就是可以通过指针来修改原值,有了这个特性,那么多级指针就变得非常有意义了,如下:

  • 可变参数:

在Java中我们知道可变参数是由...来弄的,其实在C中也类似,其中我们经常打印的printf()函数就接收一个可变参数,查看一下源码便知:

所以咱们也来弄一个可变参数:

参数中不能只有可变参数,必须要有一个确定参数,所以修改如下:

接着问题来了,如何来取出可变参数的值呢?看下面:

然后接着进行遍历,根据类型:

注意:其确定参数给NULL值是可以的,反正是要有一个,什么类型的都可以,不能没有确参,如下:

函数指针

定义:指向函数的指针。

其中"void (p) (char)"就是一个函数指针,void表示该函数无返回值;(char*)表示函数的参数列表,目前只接收一个参数;(*p)表示指向函数的指针。

其实也就相当于Java中的方法回调的意思,另外可以将函数的声明定义成一个typedef,如下:

可以用函数指针模拟HTTP请求,如果成功就执行某个函数,失败则执行某个函数,如下:

预处理器

预处理器主要是完成文本替换的,常用的预处理器如下:

  • #include:这个就不多说了。
  • #if、#elif、#else、#endif:在实际代码编写中会遇到这样的写法,如下:

假如不想要这段代码了,则直接更改条件既可:

适用的场合就是假如写的代码不想要了,则不用注释掉了。

  • #define、#ifdef、#ifndef:这里可以配合#define的宏定义来配合上面的一些条件来使用,如下:

其中定义的宏是可以被取消的,如下:

其中#define宏定义分为两种:宏变量和宏函数,具体如下:

这样在代码中就可以使用I来表示1了,如下:

而在之前说过预处理其实也就是做文本替换用的,所以代码中所有的I就会被预处理器替换为1。

接下来看一下宏函数:

此时就可以在代码中进行调用了,如下:

但是宏函数也有陷阱需要注意,看下面这个:

如果修改一下:

期望的结果应该是(1 + 10)* (10 + 10) = 220,但是运行看:

居然变成了:1 + 10 * 10 + 10了,所以需要特别注意,可以加个括号解决:

下面来看一下宏函数有哪些优缺点: 优点:它只是文本替换,使用到宏函数的地方会执行替换,不会有函数调用的开销(将参数压栈,释放栈之类的)。 缺点:1、不会对我们的代码执行检查,不像普通的函数在编写阶段就会给出相印的错误提示。2、假如宏函数是一个非常复杂的函数,那么每个调用它的地方就会完全替换,造成代码冗余使得最终生成的目标文件(如so)增大了,比如:

如果代码中调了两次它,如下:

实际上文本替换之后就是:

其实内联函数跟宏函数的执行模式是一样的,也是执行代码替换,但不是一个概念,内联函数在编写时会做检查,另外它里面的代码不能编写过于复杂的代码,如使用了switch、while等复杂控制逻辑,否则会将内联函数降级为普通函数,那何为内联函数呢?其实就是inline关键字,如下:

  • #pragma:这个用得较少,在VS中在头文件中会自动有一个如下东东:

它表示该头文件只能被引用一次,其实通用的写法是用它:

其效果都是一样的。

自己实现sprintf功能

自己实现一个只考虑传整型参数的情况就成,那如何来实现呢?下面开始:

如果遇到了“%”,则需要判断一下它的下一位字符是否是“d”字符,只有这样才是一个合法的占位,所以:

然后如果发现此参数是一个负数,则需要前面手动加一个“-”,如下:

然后再将解析到的字符串参数遍历到结果串当中,如下:

下面使用一下咱们自己编写的函数看下效果:

原来是少了这么一句关键逻辑,如下:

//
// 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
复制代码

此时再编译运行:

结构体

结构体是C编程中一种用户自定义的数据类型,类似于Java的JavaBean: 下面来定义一个结构体:

其中注意:结构体中的所有变量都是public的,下面来使用一下:

也可以在定义struct时就指出变量,如下:

另外还有一个定义方法,采用宏,如下:

字节对齐

默认对齐方式: 先来问一下对于上面这个结构体,它占多少字节呢,咱们可以用sizeof来打印一下:

int不是占4个字节、short占2个字节,加起怎么的也不可能是8个字节呀,下面再来定义两个结构体查看一下它的字节大小:

应该是2+2+4=8个字节嘛,为啥第二个还等于12个字节呢?这里也就是对于c中结构体还存在一个字节对齐的概念,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址开始访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。字节对齐的问题主要就是针对结构体。 如果没有手动指定字节对齐方式,就会按自然对齐进行字节对齐,那自然对齐是按什么规则对齐的呢?如下:

1、某个变量存放的起始位置相对于结构的起始位置的偏移量是该变量字节数的整数倍。 如何理解,咱们结合代码来理解一下: 先来看MyStruct1,它占的字节长度为8,咱们可以把里面元素的地址给打印出来便于分析:

结合这个理论来理解一下,对于MyStuct1这个结构体的起始位置也就是它里面第一个元素i的起始位置为:

而对于第二个元素j,是一个short类型,占2个字节,那么其起始位置必须是2的整数倍,所以刚好是:

所以从地址开始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的倍数,不能是奇数,如下:

共用体

定义:在相同的内存位置存储不同的数据类型,共用体占用的内存应足够存储共用体中最大的成员。

如果咱们改变一下j的值,看结果会有啥变化:

咱们将i和j的地址打印一下就明白了:

所以当改了j之后,其i的值也被更改了。

那。。共用体有啥作用呢?其实也就是节省内存,比如说程序中要使用到这三个变量:

int i = 0;

int j = 0;

int k = 0;

然后同一时刻这三个变量只会用其中一个,那如果没有共用体的话,那每个变量都会占用4个字节,而如果将其定义到共用体当中,那只会占用4个字节的空间,相当于内存复用。

C++开端

输出

首先的话题就是输出:C使用printf向终端输出信息,而C++提供了 标准输出流,比较简单,如下:

函数符号兼容

我们知道对于C的大部分代码可以在C++中直接使用,但是仍然有需要注意的地方。 咱们先新建c文件,定义一个函数:

然后我们在cpp中来使用c中的函数:

编译一切正常,来运行一下:

此时需要加一句这个来解决:

那"extern c"的作用是啥呢?为了说明这个问题,下面来做一个实验: 先在目录中建立一个main.c文件,如下:

然后里面的代码为:

接着再生成一个cpp文件,里面的内容也是跟main.c一模一样:

接下来用gcc来分别编译一下

接着咱们用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'终止的一维字符数组。表现形式如下:

②、字符串操作: 这里主要是对常用的一些字符串操作函数罗列一下,待实际使用时查找:

③、C++ string类: C++ 标准库提供了 string 类类型,定义在头文件string当中,支持上述所有的操作,另外还增加了其他更多的功能。有如下使用形式:

【注意】:如果是new出来的堆内存需要通过delete来释放,而malloc出来的堆内存则需要通过free来释放。 而在C++使用string可以进行如下操作:

其中如果是在堆中new的string,调用相应的方法就得用"->"来操作了,如下:

其中对于字符串指针在传输时效率比直接传字符串对象要高,所以可以根据实际需要动态选择。

命名空间

namespace 命名空间 相当于java的package,下面来使用一下:

那如果想要调用此test()方法,直接调用肯定是不行的,如下:

需要加域作用符:

而命名空间是可以嵌套的,如下:

而如果想在调用时不加域作用符就可以用using,类似于java中的import操作,如下:

而如果想完全不写域作用符,那还可以这样:

所以基于此,对于之前我们编写代码中使用了好多域作用符,如下:

简化的话就是使用using来包含std这个命名空间,如下:

对于域使用符还有另外一个作用,如下:

那如果想输出全局变量的ii该怎么慢,此时就可以使用域作用符,如下:

也就是当全局变量在局部函数中与其中某个变量重名,那么就可以用::来区分。

C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,用户定义的类型。

咱们来新建一个头文件来定义一下类:

其访问修饰符具体含义如下:

  • private:可以被该类中的函数、友元函数访问。 不能被任何其他访问,该类的对象也不能访问。

  • protected:可以被该类中的函数、子类的函数、友元函数访问。 但不能被该类的对象访问。

  • public:可以被该类中的函数、子类的函数、友元函数访问,也可以被该类的对象访问。

其中构造函数和析构函数目前还木有实现,其实可以在声明的时候就定义实现,如下:

也可以在cpp中进行实现,新建一个cpp文件:

其中类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行(不需要手动调用)。

另外如果想通过构造方法来给成员变量赋值,当然可以跟java一样如下弄:

但是在cpp中还有它独特的地方就是在声明构造时就可以给成员变量进行赋值,表现如下:

此时实例化就得传两个参数了:

常量函数

函数后写上const,表示不会也不允许修改类中的成员。下面来看看:

声明一个给成员变量赋值的方法,没啥问题,但是!!如果给这个函数的后面加一个const修饰,情况就不一样啦,如下:

其错误提示为:

比较简单,也就是如果发现该函数只能读不会写,则可将其声明为常量函数。

友元 

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

  • 友元函数

这个是有别于Java的,假如咱们想通过这个函数来修改类中的私有成员,如下:

此时就需要将这个函数声明为友元函数,具体做法如下:

此时方法就不报错了:

咱们来使用一下:

编译运行:

  • 友元类

此时就可以将Teacher声明为Student类中的友元类,如下:

也就是当Teacher声明为Student的友元类时,则Teacher中所有的函数及成员都是Student中的友元了。

静态成员

和Java一样,可以使用static来声明类成员为静态的,当我们使用静态成员属性或者函数时候 需要使用 域运算符 ::,下面咱们以单例模式为例:

在头文件中声明一个Instance类:

其中需要注意的是,对于静态成员是需要进行初始化的,可以在头文件中声明时就初始化,也可以在.cpp中进行,这里以cpp的方式来对成员变量进行初始化,如下:

下面来具体实现getInstance()方法:

我们知道对于Java来说这样写是会有线程安全问题,那对于C++11编译器来说会保证内部静态变量的线程安全的, 当然可以加锁,关于多线程的问题在之后会学习到,这里先不管多线程的情况,下面来使用一下这个单例:

重载函数

C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分为函数重载和运算符重载。

  • 函数重载

  • 操作符重载

这个是C++独有的,C++允许重定义或重载大部分 C++ 内置的运算符 ,函数名是由关键字 operator 和其后要重载的运算符符号构成的 ,重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。

成员函数:

新建一个头文件:

假设要实现这样的一个操作,如下:

接下来操作符重载就是将不可能成为可能,具体做法如下:

此时定义了+号运算符重载之后,之前的对象相加操作就可以正常啦,看下结果:

那为啥我们还能在表达式中看到正常的结果呢?

这是因为对于这个表示式底层是存在对象拷贝操作的,如下:

那何以见得呢?咱们可以先定义一个拷贝函数来打印一下是否如猜想:

所以定义一个无参的构造方法:

此时咱们来运行看一下是否调用了两次拷贝构造:

其实是因为:

由于程序是在mac上跑的,可见是对返回值是做了RVO优化,从而是看不到拷贝构造函数的调用了。 总之记住一点:本来是要进行两次对象拷贝的,但是编译器做了一些优化最终会减少或者完全看不到对象的拷贝迹象了。

非成员函数:

新声明一个类:

接着来声明操作符重载,这里不需要定义到Test2类的内部了,直接在我们调用的外部中来声明,如下:

然后运行看一下结果:

其中允许重载的运算符如下:

比如说重载new、delete如下:

void *operator new (size_t size)
{
    cout << "新的new:" << size << endl;
    return malloc(size);
}

void operator delete(void *p)
{
    //释放由p指向的存储空间
    cout << "新的delete" << endl;
    free(p);
}
复制代码

继承

跟java一样,类也是存在继承的,范例:

class A:[private/protected/public] B

默认为private继承 

A是基类,B称为子类或者派生类 
复制代码

下面来看下具体代码:

那下面来使用一下:

此时涉及到继承的访问修饰符了,由于默认情况下是private继承,所以既使父类的方法是public的子类也无法访问,要访问就得将继承修饰符改一下:

其中涉及到继承修饰符如下:

另外有一点是跟java不同的,java只支持单继承,而c++是支持多继承的,如下:

下面来看一下打印:

此时调用子类的test()会打印啥呢?如下:

而如果子类想调用父类的方法该如何写呢?

编译运行:

那如果将子类的test()方法删掉又会怎样呢?

从错误提示来看是因为在父类中定义有多个test()方法,咱们删掉其中一个基类的test()方法:

此时就正常了,再次编译运行:

从这个就可以看出为啥刚才报基类有多个test()的异常,是因为如果子类没有实现父类的方法,那么就会调用父类的,但是父类定义了两个test()方法,所以编译器不知道调哪个了,所以就报错了。

多态

多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

静态多态(静态联编)是指在编译期间就可以确定函数的调用地址,通过函数重载和模版(泛型编程)实现,那下面来举个例子来体会一下静态多态的表现:

此时打印的是:

动态多态(动态联编)是指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定 ,通过继承+虚函数 实现,下面来看下:

其调用方式不变:

编译运行:

此时就调用的真正类型的方法,基于此,有下面一个原则:

构造函数任何时候都不可以声明为虚函数。

析构函数一般都是虚函数,释放先执行子类再执行父类,如果不声明为虚函数,则在多态中只会调用父类的析构函数了。
复制代码

另外还有一种纯虚函数,其意义跟java中的abstract抽象方法类似,具体如下:

此时如果运行就会报错:

也很容易理解,跟java的一样,纯虚函数是一定需要在子类中进行实现的,如下:

此时编译就不报错了。

模板

模板是泛型编程的基础
复制代码
  • 函数模板【类似于java中的泛形方法】
函数模板能够用来创建一个通用的函数。以支持多种不同的形參。避免重载函数的函数体反复设计。
复制代码

其中有个等价的写法:

  • 类模板【类似于java中的泛型类】
为类定义一种模式。使得类中的某些数据成员、默写成员函数的參数、某些成员函数的返回值,能够取随意类型

常见的 容器比如 向量 vector 或 vector 就是模板类
复制代码

容器

常用的数据结构包括:数组array, 链表list, 树tree, 栈stack, 队列queue, 散列表hash table, 集合set、映射表map 等等。容器便是容纳这些数据结构的。这些数据结构分为序列式与关联式两种,容器也分为序列式容器和关联式容器。

STL 标准模板库,核心包括容器、算法、迭代器。

序列式容器/顺序容器:

  • 向量(vector):连续存储的元素,后进先出。

另外还有一些重载的形式,如下:

有了容器之后就可以往里面添加、删除数据了,下面来用一下:

接下来删除一个元素:

那最终容器里还剩哪个元素呢?下面就可以将元素输出出来,如下:

另外有个细节想一下,vector是一个模板类,为啥我们可以用下标的形式来访问它里面的元素,很显然是因为该类重写的下标运算符嘛,源码定义如下:

另外通过下标获取元素还有另外一种方式,如下:

另外还可以通过函数直接获得队首和队尾的两个元素,如下:

如果想清空元素可以用:

还有另外一种清除函数,可以删除某个元素,如下:

那vector目前的容量是多大呢?也有现成的函数,被清掉之后很明显大小为0嘛,试试看:

这里可以说明vector容器其内存占用的空间是只增不减的,clear()释放元素之后,去不能减小vector所占的内存空间。那这不会造成内存的浪费么?尤其对于全局的vector变量,然后方法使用到了该全局变量,如下:

此时全局的容量大小虽说在方法内部被clear()掉了,但大小还是2,那实际如果被调多次那容量不会很可怕,那怎么能达到缩小容量大小的效果呢?这里可以采用swap替换操作,用一个新的临时vector来替换目前的vector,具体做法如下:

其中还可以简写:

注意:建立临时vector temp对象,swap调用之后对象vec占用的空间就等于默认构造的对象的大小,temp就具有vec的大小,而temp随即就会被析构,从而其占用的空间也被释放,所以不会有内存泄漏问题。

那如果要遍历里面的所有元素可以使用迭待器,具体使用如下:

其中在迭待过程中是不能删除元素的,跟java一样的,另外还需要了解到:

  • 列表 (list):由节点组成的双向链表,每个结点包含着一个元素。
  • 双端队列(deque):连续存储的指向不同元素的指针所组成的数组。
  • 队列(queue):先进先出的值的排列。

  • 栈(stack):后进先出的值的排列。

  • 优先队列(priority_queue ):元素的次序是由所存储的数据的某个值排列的一种队列。

也就是说明默认情况下最大的元素在队首,那如果想改变默认的元素排序将最小的放到队首呢,具体就得这么办:

其中:

当然这个less和greater都是一个模板结构体 也可以自定义,就类似于Java中的比较器一样,这样less和greater的含义就发生变化了,如下:

less 让优先队列总是把最大的元素放在队首;

greater 让优先队列总是把最小的元素放在队首;

下面用自定义的类型来自定义一下比较器,如下:

编译运行:

报错了,为啥?因为对于Student类来说不知道如何进行排序,所以此时就得自定义排序规则,这里校仿一下它:

打开看一下它的源码实现:

咱们对Student的自定义排序直接用这个源码,当然由于我们只对Student类型进行排序,所以就用不着泛型了,具体定义如下:

关联式容器

关联容器和大部分顺序容器操作一致

关联容器中的元素是按关键字来保存和访问的 支持高效的关键字查找与访问
复制代码
  • 集合(set):由节点组成的红黑树,每个节点都包含着一个元素,元素不可重复。

如果要删除元素可以用:

那如何遍历set呢?这里需要特别注意,有别于vector的,我们知道vector的遍历是如下:

那校仿一下来遍历set:

证明set中并没有重写<号运算符,那只能将其换成不等于了,如下:

  • 键值对(map):由{键,值}对组成的集合。

其插入元素还可以生成一个键值对对象,如下:

如何通过key来查询元素呢,如下:

删除元素则如下:

最后来遍历一下元素,如下:

类型转换

除了能使用c语言的强制类型转换外,还有:转换操作符 (新式转换)

const_cast

修改类型的const或volatile属性。

static_cast

  • 基础类型之间互转。如:float转成int、int转成unsigned int等
  • 指针与void之间互转。如:float转成void、Bean转成void、函数指针转成void*等
  • 子类指针/引用与 父类指针/引用 转换。

但是如果将父类的方法声明为虚方法,结果就不一样了,如下:

dynamic_cast

主要将基类指针、引用 安全地转为派生类,在运行期对可疑的转型操作进行安全检查,仅对多态有效。 还是拿static_cast的程序进行举例,咱们先将父类的虚函数去掉再做实现:

然后编写动态转换代码:

错误提示为:

Parent不是多态,也就是需要将它声明为虚函数才行,所以增加virtual关键字:

因为转换失败了,这时指针会为null,所以完善的代码应该加上判断:

接下来修改一下程序:

那如果是子类动态转换成父类呢?

reinterpret_cast

对指针、引用进行原始转换

char*与int转换

异常

另外,相比java,c++中抛异常时可以随便抛一个对象,不一定是非要继承至exception类的,如下:

文件与流操作【基本C方式使用得多一些】

C 语言的文件读写操作

头文件:stdio.h

函数原型:FILE * fopen(const char * path, const char * mode); 

path: 操作的文件路径

mode:模式
复制代码

其中的mode有如下这些:

下面先来写一个文件:

打开文件看一下内容:

另外还可以以格式化的文本方式来写入,如下:

接下来读取文件:

这是因为当读取遇到空格字符就会终止,所以需要遍历读取,如下:

另外还有一个直接读最大字节的API,对于上面的程序可以改为它:

下面把范例贴一下:

C++ 文件读写操作

<iostream> 和 <fstream>
复制代码

其涉及到的方法有:

下面举个例子来简单的使用一下:

线程

C++11线程

在C++11中如何来创建一个线的,如下:

POSIX线程【常用】

POSIX 可移植操作系统接口,标准定义了操作系统应该为应用程序提供的接口标准。相比C++ 11的线程这种方式就要麻烦一些,下面来用看一下它的创建方式:

然后第三个参数则为线程运行的函数地址,其参数类型为:

也就是是个函数指针,该函数返回一个void指针,接收一个void参数,所以咱们来定义一下该参数:

最后一个参数则是运行函数的参数,所以咱们也来定义一下:

编译运行:

  • 线程属性 线程具有属性,用 pthread_attr_t 表示,目前咱们在创建线程时这块的参数是传的0,如下:

接下来咱们来定义一下该参数:

那设置该属性有啥用呢,主要是有以下两种用处,如下: ①、分离线程【了解既可】:

线程创建默认是非分离的,当pthread_join()函数返回时,创建的线程终止,释放自己占用的系统资源

分离线程是指不能被其他线程等待,pthread_join无效,线程自己玩自己的。
复制代码

在设置属性之前咱们先来看一下目前程序的行为:

在默认属性的情况下我们的join()是能正常等到线程结束之后才结束的,如下:

接下来设置成分离线程属性,如下:

再来编译运行:

可以看到我们的join()已经不起作用了,这个了解一下既可,在Android NDK中实际中基本上很少用到它。

②、调度策略与优先级【实际用得少,了解既可】:

线程同步

多线程同时读写同一份共享资源的时候,可能会引起冲突。需要引入线程“同步”机制,即各位线程之间有序地对共享资源进行操作。 下面先看下有线程同步问题的代码:

编译运行:

接下来可以给程序加入互斥锁,具体做法如下:

此时再编译运行:

其实跟java的synchronized是一样的效果。

条件变量

条件变量是线程间进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立",从而唤醒挂起线程。 下面使用它来完成一个生产者消费者模型,也就是如果发现队列里没有数据可消费了则等待生产者进行生产,一旦生产了数据则消费者立马消费,具体实现如下:

接下来实现具体的插入元素及取出元素:

当队列中没有数据时,目前方法是直接就结束,并没有一个等待的过程,在Java中我们可以使用wait和notify机制达到,而在C++中可以使用条件变量来搞定,具体做法如下:

同样的条件变量也是需要初始化和销毁的,下面来使用它:

接下来咱们来测试一下:

编译运行:

注意:条件变量等待会让cpu释放锁,而不是一直在背后运行,能提高性能。

智能指针

自C++11起,C++标准库提供了两大类型的智能指针。为啥要有智能指针呢?下面先来用程序来说明一下背景:

但是!!

对于堆上的内存都需要我们手动去释放内存,如下:

也就是在实际开发中会存在堆中申请的内存可能会忘记手动释放而造成内存泄漏,所以智能指针的出现就是用来解决这个问题的。

shared_ptr

操作引用计数实现共享式拥有的概念。多个智能指针可以指向相同的对象,这个对象和其相关资源会在最后一个被销毁时释放。 下面咱们来使用一下它:

其实它内部是实现了引用计数,像下面这个程序:

而当testPtr()方法执行完之后,shared_ptr1和shared_ptr2都会回收,因为是栈上的内在,所以当shared_ptr1被回收时其引用计数由2就会变为1,同样的shared_ptr2被回收时其引用计数就由1又变回了0,而当引用计数为0时则会delete到所引用的A对象了。

注意:虽然使用shared_ptr能够非常方便的为我们自动释放对象,但是还是会出现一些问题。最典型的就是循环引用问题。

下面来看一下下面的代码:

结果一切正常,没有内存泄漏,但是如果下面这样来改就会有问题了,如下:

所以基于这个问题,weak_ptr就诞生了。

  • weak_ptr:

weak_ptr是为配合shared_ptr而引入的一种智能指针。主要用于观测资源的引用情况。

它的构造和析构不会引起引用记数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。

配合shared_ptr解决循环引用问题,下面来对上面有问题的代码进行修改,如下:

注意:weak_ptr 提供expired 方法等价于 use_count == 0,当expired为true时,lock返回一个存储空指针的shared_ptr。

unique_ptr

实现独占式引用,保证同一时间只有一个智能指针指向内部对象。什么意思?看代码:

自定义智能指针

为了更加深入的理解“shared_ptr”这个智能指针的原理,咱们动手来写一个类似效果的智能指针,先新建一个头文件定义大体的框架:

还得实现一个拷贝构造函数:

咱们来使用一下:

那如果调用时这样来写呢?

此时就需要重载一下=号运算符了,如下:

编译运行:

下面再来理解一下这段代码,为啥要引用计数要减1,如下:

这时因为:

因为在默认构造中是这么写的:

然后此时要将该对像进行如下新的赋值:

那在赋值之前,不得先将shared_ptr2这个新对象中的值给清掉么,这样说还不太明显,下面来改造一下程序就立马能明白了:

此时shared_ptr2又要引用shared_ptr1,如果不先释放shared_ptr2原来的值,那不就造成了a2有内存泄漏么,试一下:

编译运行:

所以还是将代码还原,再运行:

编译运行:

总之:其实智能指针底层就是用了引用计数,并利用栈内存出方法必然会释放的原因来实现的。

部分C++11、14特性【不一定自己会使用这些特性,但是别人使用到了需要能读懂】

nullptr

nullptr 出现的目的是为了替代 NULL。 C++11之前直接将NULL定义为 0。

所以为了解决这个问题,可以用nullptr,如下:

类型推导

C++11 重新定义了auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。

而对于vector的遍历也可以用auto来简化,具体如下:

基于范围的for循环

实际上就是foreach,新式写法可以这样写了:

Lambda:【懂java8的应该这块不难理解】

匿名函数,即没有函数名的函数 

完整形式:

捕获外部变量列表 mutable exception->返回类型 { 函数体 }

mutable:在外部变量列表以值来捕获时,无法修改变量的值,加上mutable表示可修改(不会影响外部变量)
复制代码

这里以创建线程为例,原来的方式我们已经比较清楚了,看一下使用Lambda表达式简化成了啥样:

其中这块还有一些其它的形式:

如果我们想在线程中引用外部的变量,目前的写法是不行的,如下:

此时如果使用它:

就可以正常引用了,如下:

但是此时如果我们想在线程中去修改外部变量的值呢?

此时另外一种形式就发挥作用了,如下:

修改一下代码:

此外如果方法有值返回可以这样写:

但是注意:这里的"-> auto"自动推导在C++11是不支持的,而在C++14中是支持的。

gcc/g++/clang,相当于javac

了解c/c++编译器的基本使用,能够在后续移植第三方框架进行交叉编译时,清楚的了解应该传递什么参数。

  • clang

clang 是一个C、C++、Object-C的轻量级编译器。基于LLVM (LLVM是以C++编写而成的构架编译器的框架系统,可以说是一个用于开发编译器相关的库)

  • gcc

GNU C编译器。原本只能处理C语言,很快扩展,变得可处理C++。(GNU计划,又称革奴计划。目标是创建一套完全自由的操作系统)

  • g++

GNU c++编译器

  • gcc、g++、clang都是编译器。

    • gcc和g++都能够编译c/c++,但是编译时候行为不同。这块需要特别的注意,并非gcc是为c而生,而g++是为c++而生的。

    • clang也是一个编译器,对比gcc,它具有编译速度更快、编译产出更小等优点,但是某些软件在使用clang编译时候因为源码中内容的问题会出现错。

    • clang++与clang就相当于gcc与g++。

  • 对于gcc与g++:

    • 后缀为.c的源文件,gcc把它当作是C程序,而g++当作是C++程序;后缀为.cpp的,两者都会认为是c++程序

    • g++会自动链接c++标准库stl,gcc不会

    • gcc不会定义__cplusplus宏,而g++会

编译器过程

一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)、和连接(linking)才能变成可执行文件。

  • 预处理

gcc -E main.c -o main.i

-E的作用是让gcc在预处理结束后停止编译。

预处理阶段主要处理include和define等。它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。

  • 编译阶段

gcc -S main.i -o main.s

-S的作用是编译后结束,编译生成了汇编文件。

在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

  • 汇编阶段

gcc -c main.s -o main.o

汇编阶段把 .s文件翻译成二进制机器指令文件.o,这个阶段接收.c, .i, .s的文件都没有问题。

  • 连接阶段

gcc -o main main.s

链接阶段,链接的是函数库。在main.c中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明。系统把这些函数实现都被做到名为libc.so的动态库。

函数库一般分为静态库和动态库两种:

  • 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。Linux中后缀名为”.a”。
  • 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。Linux中后缀名为”.so”,如前面所述的libc.so就是动态库。gcc在编译时默认使用动态库。

静态库节省时间:不需要再进行动态链接,需要调用的代码直接就在代码内部。

动态库节省空间:如果一个动态库被两个程序调用,那么这个动态库只需要在内存中。

关于这两者的区别咱们用一个具体的例子来形象的说明一下:

1、假如有一个静态库a.a,它里面包含一个test函数,然后有个源文件source.c,它里面有个test1函数,而这个源文件需要链接a.a这个静态库,当编译完成之后source.a里面就拥有了test+test1两个函数了,也就是编译期就将所有的符号加入到.so库中。

2、假如有一个动态库a.so,它里面包含一个test函数,然后有个源文件source.c,它里面有个test1函数,而这个源文件需要链接a.o这个动态库,当编译完成之后source.a里面就只有一个test1函数,在运行时会动态的加载a.so。

注:Java中在不经过封装的情况下只能直接使用动态库,也就是说:

转载于:https://juejin.im/post/5ce4fe3ff265da1ba431c431

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值