此篇笔记初步介绍了C++和C语言的些许差异,以及C++中典型语法,方便后续的C++学习,可以根据需要跳转到对应的部分进行学习。
目录
一、C++为何而来?
1、C语言的三方冲突:
库--我--同事,在大型工程中,一旦出现两个人命名相同的话就会非常尴尬,毕竟写好一个功能,一个函数或者一个变量复用的次数很多,谁都不愿意去改,毕竟这是一个不小的工程量,十分的不方便,而这一问题在C++中得以解决。
2、C++解决方法:域作用限定符“::”
namespace Hanluo//此位置一般使用自己的名字,这就是你的域
{
int char = 'a';
int rand = 0;
}
int mian(void)
{
printf("%d\n",Hanluo::rand);//意味着这个变量是Hanluo里的
return 0;
}
命名空间中可以定义函数、变量、类型
3、命名空间展开:
命名空间全部展开和部分展开:
using namespace Hanluo;//全部展开(授权)
using Hanluo::rand;//部分展开(授权)
当多个头文件中创建不同的命名空间时,在.c文件中引用这些头文件全不会产生冲突,编译器会主动合并同一个命名空间,因此,(不同头文件同一命名空间不能存在同一个变量名或函数)
4、流提取和流插入
来到C++,我们不再使用#include<stdio.h>
而是换作#include<iostream>这是C++的库。
我们还会认识到一个新的语句endl,相当于C中我们使用的换行功能。
其次就是用以下两种方式,你可以成功取代scanf和printf
#include<iostream>
namespace Hanluo
{
int data = 10;
};
int main()
{
using std::cout;
cout << "abcdefg!\n" << "higklmn!" << std::endl;
cout << Hanluo::data << std::endl;
std::cin >> Hanluo::data;
cout << Hanluo::data;
}
二、缺省参数
全缺省:
所有参数都有一个“默认备用值”,预防使用者忘记初始化导致的麻烦。
半缺省:
并非所有参数都给了“备用值”,
//初始函数
Func1(int i ,int k)
{
for(int j = 0;j < i - k;j++)
{
cout << j << endl;
}
}
//全缺省函数
Func2(int i = 2 ,int k = 1)
{
for(int j = 0;j < i - k;j++)
{
cout << j << endl;
}
}
//半缺省函数
Func3(int i,int k = 1)
{
for(int j = 0;j < i - k;j++)
{
cout << j << endl;
}
}
如果,在主函数中只传一个参数,函数2和函数3都是将参数传给i并且只能传给i,除非出现参数类型不同的情况,此时可以根据类型来判断传参是传的哪个参数;如果传两个参数则所有函数中i和k都会被初始化;如果都不传参,只有函数2可以不出意外的完成,其他的两个函数则没有保障。
不允许声明和定义同时给缺省参数
如果给,声明给,定义不给
因为在头文件中一班为声明,头文件一般不会重复,所以大工程后期会做去重
这里还有一个之前的知识点,static修饰函数,使得当前函数只能在该文件中使用,防止出现访问到不该访问的位置造成错误。
如果你要问怎么实现,很简单,在函数传参中直接给参数赋值即可,在没有传参时编译器会考虑它的。
三、重载:一词多义
重载前提:函数名可以相同,但是参数类型,数量,顺序必须不同;且在同一个作用域
1、C++关键字:
C语言拢共32个关键字,而C++拥有64个,数量翻了整整一番。
C++关键字一览:
asm | do | if | return | try | continue | auto |
double | inline | short | typedef | for | bool | dynamic_cast |
int | signde | typeid | public | break | else | long |
sizeof | typename | throw | case | enum | mutable | static |
union | wchar_t | catch | explicit | namespace | static_cast | unsigned |
default | char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register | const |
false | private | template | void | true | const_cast | float |
protected | this | volatile | while | delete | goto | reinterpret_cast |
2、函数重载原理
对同一个函数名的函数,根据不同的使用方法,在编译过程中使用不同的地址编号,做到对函数的区分
四、引用
1、概念:
引用不是定义一个变量,而是给已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间,他和被他引用的变量公用一块空间
2、使用方法:
在类型紧邻的位置加上“&”,这样便算作引用。
//举例
int mian(void)
{
int a = 0;
int& b = a;//引用
cout << &a << endkl;\\取地址符
cout << &b << endl;
}
typedef struct ListNode
{
int data;
struct ListNode* next;
}ListNode,*PListNodde;
v3333333333333333333333333333333oid PushBack(ListNode*& phead,int x)\\引用传参,可以省略二级指针
{
//~~~~
}
void PushBack(PListNode& phead,int x)
{
//~~~
}
3、特性:
1.引用在定义时必须进行初始化
2、一个变量可以有多个引用
3、引用一旦引用一个实体,就不能再引用其他实体
void Text()
{
int a = 7;
// int& ra; //该语句会引起编译报错
int& ra = a;
int& rra = a;
printf("%p %p %p\n",&a, &ra ,&rra);
}
4、常引用
void Text(void)
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同const int& rd = d
}
(1)常引用使用场景
- 做参数
- 做返回值
传值返回:
在栈帧中创建一块区域或者使用寄存器将变量进行临时存储,避免出了作用域后消失的情况。
传引用返回:
当函数栈帧销毁时,空间仍然存在,我们使用“引用”这把钥匙去访问这个空间,可能出现无法预知的后果。类似于野指针,在房间1退还之前留存了一把钥匙。
函数调用先传参
所以调用以下代码时会出现不同的值的情况
因为之前的空间被覆盖,覆盖之前采用一次,覆盖之后再采用一次
还有如下
int& Add(int a,int b)
{
int c = a + b;
return c;
}
int main(void)
{
int& ret = Add(1,2);
Add(3,4);
cout << "Add(1,2) is:" << ret << endl;
//结果大家可以猜一下
}
答案隐藏在下方紫色方块中,滑动鼠标选中即可看到。
结果是7
5、C++接口设计:
//读取或者修改第i个位置的值
int& SLAT(struct SeqList& ps,int i)
{
assert(i < ps.size)
return ps.a[i];
}
int main(void)
{
struct SeqList s;
SLAT(s,0) = 1;
cout << SLAT(s,0) << endl;
return 0;
}
变成类的写法:
struct SeqLiist
{
//成员函数
int& at(int i)
{
assert(i < size)
return a[i];
}
//成员变量
int a[N];
};
int main(void)
{
//struct SeqList s1;//兼容c的用法
SeqList s2;
for(size_t i = 0;i < N;i++)
{
s2.at(i) = i;
}
for(siize_t i = 0;i < N;i ++)
{
cout << s2.at(i) << " ";
}
cout << endl;
return 0;
}
6、小总结:
传引用传参(任何时候都可以使用)
1、提高效率
2、输出型参数(形参的修改,影响的实参)
传引用返回(出了函数作用域对象还在才可以使用)
1、提高效率
2、修改返回对象
7、引用的本质:
通过观察指针和引用的汇编逻辑,我们发现他们的实现是一样的,但是二者在概念上不同,我们应该牢记引用是不占用空间的,而指针式占据空间的。他们的汇编语言展示如下。
00123456 lea eax,[a]
00123459 mov dword ptr [p1] ,eax
00123556 lea eax,[a]
00123559 mov dword ptr [p1] ,eax
以下是他们的不同点:
1、引用,概念上是一个变量名,而指针是存储一个变量地址
2、引用在定义时必须初始化,指针并没有要求
3、引用在初始化时引用一个实体后,就就不能再引用其他实体,而指针可以在任意时候指向任意实体
4、没有NULL”引用“,但又有NULL“指针”
5、在sizeof中=含义不同:“引用”的结果为引用类型的大小,而指针始终是地址空间所占用的字节数(32位平台下4字节)
6、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7、有多级指针,但是没有多级引用
8、访问实体的方式不同,指针需要显式解引用,引用编译器自己处理
9、引用比指针使用起来相对安全
五、内联函数
在了解这个概念之前,我们先回顾一下“宏”这个概念,宏的一种很直观的描述就是“直接替换”,接下来做一个小测试,你可以尝试使用宏来实现Add(a,b)的功能。
代码如下:
//定义一个宏,实现ADD
#include<stdio,h>
#define ADD(x,y) ((x)+(y))
#defiine Mul(x,y) ((x) * (y))
//测试用例
int a = 1;
int b = 2;
ADD(a | b,a & b );//
ADD(a * b,b * a)
Mul(a + b,b + a)
1、宏的缺点:
- 容易出错
- 不能调试
- 没有类型安全的检查
2、宏的优点:
- 针对频繁调用的小函数,可以有效减少建立栈帧的次数,提高了效率
- 适用于不同的类型,没有严格的类型限制
回顾了一下“宏”我们来正式看一下内联函数,他们之间有着相似之处。
3、内联函数--inline
- inline是一种空间换时间的做法,如果编译器将函数当做内联1函数处理,那么在编译阶段,会用函数体替换函数调用,缺点是可能会使目标文件变大,优点是少了调用的时间花费,提高程序运行效率,
- 其对于编译器而言只是一个建议,不同编译器关于inline的实现也不尽相同,。一般建议:将函数规模较小而且不是递归并且频繁调用的函数采用inline修饰,否则编译器会忽略其特性。
- 内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联展开。
长函数使用内联会导致函数膨胀
不能声明和定义分离在两个文件里!
用关键字inline修饰的函数就是内联函数。关键字在函数声明和定义的时候都要加上,不写系统还是会当成常规函数
会出现链接错误:
内联函数在符号表中没有地址
会直接在调用的地方展开
归根接地内联函数出现这种情况的原因就是它的底层逻辑和普通函数不同,普通函数的调用会根据符号表中地址去调用生成栈帧,而他在调用过程中直接展开代码加到主函数中去,不会生成符号表中的地址,一旦调用它,就会去声明找它,但是它没有标记地址,所以找不到定义位置,所以他的定义和声明在不同文件里时会造成链接报错;如果在同一文件,可以直接展开然后加入到主函数的代码中去,就好比CTRL CV。
auto关键字--普通场景没有价值,类型很长才有价值
以上蓝色被选中部分就用auto直接替代了,可以很好地简化代码,他可以自动识别类型。
auto不能推倒的场景
1、不能作为函数的参数
//此处代码编译失败
void TestAuto(auto a)
{
}
2、不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
C++中
typeid:
可以拿到数据类型
cout << typeid(x).name()<< endl;
六、范围for
int arry[3] = {1, 2, 3};
for (int i : arry)
{
cout << i << ' ' << ends;
}
以上是范围for输出一维数组的样例,但是如果用C语言实现将会变得非常繁琐,还需要设置循环的次数,而这一问题C++直接自行解决了。
七、C98历史遗留问题(针对“空”的类型)
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
而在C++中引入了nullptr为了解决一些函数重载引起的问题。
nullptr的类型为void *