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申请内存之后会使用memset保存其申请的内存是一片纯白的,而不是用了之前的脏数据,因为申请内存有可能会重用之前的内存,具体用法如下:
还有一点需要注意:堆中申请的内存是不会自动释放的,需要手动去释放,如下:
其实它就等价于:
那什么场景会用到它呢,这里举一个TCP传输粘包问题,比如发送“1,2,3,4,5,6”数据,而接收的时候可能分几次才能接收完,比如是先接收到了“1,2,3”,之后再接收到了“4,5”,最后接收了“6”,至此才将数据接收完,那此时的缓冲区char首先申请的是3个字节,于是乎“1、2、3”刚好接收满了,但此时还不是一个完整的数据包,所以还得接着等“4,5,6”,当接收到了“4、5”了,就需要对缓冲区进行扩容用以存放这两个字节了,同样的最后接收到了"6",则继续再要对缓存冲再扩容一个字节。 当然直接申请一个足够大的缓存区不就不用扩容了么,这是因为数据包的大小是无法确定的,这里只是为了说明问题举了个简单的粟子而已。
而在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请求,如果成功就执行某个函数,失败则执行某个函数,如下:
预处理器主要是完成文本替换的,常用的预处理器如下:
假如不想要这段代码了,则直接更改条件既可:
适用的场合就是假如写的代码不想要了,则不用注释掉了。
其中定义的宏是可以被取消的,如下:
其中#define宏定义分为两种:宏变量和宏函数,具体如下:
这样在代码中就可以使用I来表示1了,如下:
而在之前说过预处理其实也就是做文本替换用的,所以代码中所有的I就会被预处理器替换为1。
接下来看一下宏函数:
此时就可以在代码中进行调用了,如下:
但是宏函数也有陷阱需要注意,看下面这个:
如果修改一下:
期望的结果应该是(1 + 10)* (10 + 10) = 220,但是运行看:
居然变成了:1 + 10 * 10 + 10了,所以需要特别注意,可以加个括号解决:
下面来看一下宏函数有哪些优缺点: 优点:它只是文本替换,使用到宏函数的地方会执行替换,不会有函数调用的开销(将参数压栈,释放栈之类的)。 缺点:1、不会对我们的代码执行检查,不像普通的函数在编写阶段就会给出相印的错误提示。2、假如宏函数是一个非常复杂的函数,那么每个调用它的地方就会完全替换,造成代码冗余使得最终生成的目标文件(如so)增大了,比如:
如果代码中调了两次它,如下:
实际上文本替换之后就是:
其实内联函数跟宏函数的执行模式是一样的,也是执行代码替换,但不是一个概念,内联函数在编写时会做检查,另外它里面的代码不能编写过于复杂的代码,如使用了switch、while等复杂控制逻辑,否则会将内联函数降级为普通函数,那何为内联函数呢?其实就是inline关键字,如下:
它表示该头文件只能被引用一次,其实通用的写法是用它:
其效果都是一样的。
自己实现一个只考虑传整型参数的情况就成,那如何来实现呢?下面开始:
如果遇到了“%”,则需要判断一下它的下一位字符是否是“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使用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的一样,纯虚函数是一定需要在子类中进行实现的,如下:
此时编译就不报错了。
模板是泛型编程的基础 复制代码
函数模板能够用来创建一个通用的函数。以支持多种不同的形參。避免重载函数的函数体反复设计。 复制代码
其中有个等价的写法:
为类定义一种模式。使得类中的某些数据成员、默写成员函数的參数、某些成员函数的返回值,能够取随意类型 常见的 容器比如 向量 vector 或 vector 就是模板类 复制代码