文章目录
目录
前言
终于做了这个决定好好地写博客,可能里面还有很多的不足,希望大家在评论区指正,有评论必回访。。
1.C++的输入和输出
1.1.C++头文件
要了解C++的输入和输出,首先要知道C++的头文件。
#include<iostream>
using namespace std;
c++库里的头文件,和c里面的#include<stdio.h>是一样的,输入输出流,那么第二句呢很多参考书上都有这句,这句学了命名空间就大概知道是什么意思,这就是标准命名空间std,写了这句才能直接写cout或者cin。但是这句写了不太好,因为我们,c++库的实现是定义在一个叫std的命名空间中的,这样写了会导致std这个库里的内容全部展开,这样会导致失效。但是平时练习这样写没有关系。
1.2.C++输入输出
cout << "hello world" << endl;
cout << "hello world\n" ;
printf("hello world\n");
cout就是输出,然后<<是流插入,然后那个endl相当于是换行。这个换行还有第二种写法,就是第二行的。
int i = 1;
double d = 1.11;
cout << i << d << endl;
这里我们就发现C++语法中不需要指明变量的类型,直接把变量写上去就行了。
struct Student
{
char name[20];
int age;
// ...
};
int main()
{
struct Student s = { "特莱维斯", 18 };
// cpp
cout <<"姓名:" <<s.name << endl;
cout << "年龄:" << s.age << endl << endl;
// c
printf("姓名:%s\n年龄:%d\n", s.name, s.age);
}
这里可以看出,C++并不一定比C方便,所以具体使用的时候自己考虑一下。
cin >> s.name >> s.age;
cout << "姓名:" << s.name << endl;
cout << "年龄:" << s.age << endl << endl;
scanf("%s%d", s.name, &s.age);
printf("姓名:%s\n年龄:%d\n", s.name, s.age);
cin是输入,>>是流提取运算符,这里的输入也是不用指明变量类型,直接写变量在流提取运算符后面就行了。这里注意一点就不用谢endl了。
2.缺省参数
2.1缺省参数的定义
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该 默认值,否则使用指定的实参。
2.2缺省参数的分类
缺省参数分为两类:全缺省参数和半缺省参数
**全缺省参数
void Func(int a = 10,int b = 20, int c = 30)
{
cout << "a =" << a << endl;
cout << "b =" << b << endl;
cout << "c =" << c << endl;
}
int main()
{
Func();
Func(1);
Func(1, 2);
Func(1, 2, 3);
}
大家可以看到上述图片的结果,需要说明的一点是调用函数传值得时候是从左往右传的。
不能只传给B,像这样Func(,2,);,这样是不行的。
**半缺省参数
void TestFunc(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl<<endl;
}
int main()
{
TestFunc(1);
TestFunc(1,2);
TestFunc(1,2,3);
}
这样就是半缺省参数的调用,很多同学就会问那么可不可以这样调用。
TestFunc(1, ,2);
这样是不行的,因为这样是语法错误。我给大家看一下编译器运行的结果。
那么有的同学就会问,那么定义半缺省函数可不可以这样定义。
void TestFunc1(int a, int b = 10, int c )
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl<<endl;
}
void TestFunc2(int a = 10, int b , int c)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
这样也不行,因为缺省函数是必须要从左往右开始缺省,如果像上面一段代码的编译器就会报错。
2.3缺省函数的注意事项
半缺省参数必须从右往左依次来给出,不能间隔着给
缺省参数不能在函数声明和定义中同时出现
缺省值必须是常量或者全局变量
C语言不能实现缺省函数
3.函数重载
3.1函数重载的定义
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的 形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。那么我们根据这几种情况来试验一下。
**参数个数
int Add(int left, int right)
{
cout << left + right << endl;
return 0;
}
int Add(int left,int mid,int right)
{
cout << left + mid + right << endl;
return 0;
}
int main()
{
Add(1,9);
Add(1,5,7);
}
**类型
int Add(int left, int right)
{
cout << left + right << endl;
return 0;
}
int Add(double left, double right)
{
cout << left + right << endl;
return 0;
}
int main()
{
Add(1,8);
Add(1.6,1.9);
}
**顺序
int Add(int left, double right)
{
cout << left - right << endl;
return 0;
}
int Add(double right, int left)
{
cout << left - right << endl;
return 0;
}
int main()
{
Add(1,1.9);
Add(1.9,1);
}
**特殊情况**
返回值不同,不能构成重载
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
缺省值不同,不能构成重载
void f(int a)
{
cout << "f()" << endl;
}
void f(int a = 0)
{
cout << "f(int a)" << endl;
}
构成重载,但是使用时会有问题 : f();调用存在歧义
void f()
{
cout << "f()" << endl;
}
void f(int a = 0)
{
cout << "f(int a)" << endl;
}
int main()
{
// f(); // 调用存在歧义
f(1);
return 0;
}
3.2函数重载的原理
我们应该知道C语言是不支持函数重载的,但是为什么呢?
这个时候我们应该要知道编译器的原理,分为: 预处理(头文件展开,宏替换,条件编译,去掉注释)-> 编译(检查语法,生成汇编代码)->汇编(汇编代码转为二进制代码)->链接。那么我们现在来看一个程序。
现在我们来分析C语言为什么不支持函数重载。
首先在func.c是函数定义,test.c是主函数进行函数调用,只有函数定义了才能产生地址。在test.c中,有最上面有#include“func.h”这是函数声明,在编译的时候调用函数会生成一句call_f(?) ,你可能会问为什么会是_f(),因为C语言的语法规定在编译的时候有函数声明的时候就用函数名来找。而后面的(?),是为了在链接的时候找地址用的,如果函数定义也在test.c里面,那也是不行的,test.c里面也会生成符号表,就是因为C语言的命名规则导致他生成的符号表发生歧义,具体情况参照func.c。
接下来到了func.c ,这里也有函数定义就不多说了,然后函数定义里面的内容。
而这个时候函数定义的里面会生成一个符号表。
这个时候我们做个总结。
那么现在再来讨论一下C++为什么支持函数重载,是引入了一个函数名命名规则。
首先还是test.c,不一样的是编译的时候不是_f(?),而是_Z1fi(?),为什么是这个呢?因为C语言和C++命名规则不一样,C语言是根据函数名来的,而C++根据_Z+函数名长度+函数名+参数首字母。这样就不会在链接的时候产生歧义,_Z1fi(?)这里面的问号是在链接的时候得到的地址。
然后就是func.c,生成的符号表也是不同的,这里可以很明显看出两个符号表的区别。
那么这里再看一下func.c和test.c编译的内容。
那么也来总结一下。
现在再来熟悉一下C语言和C++的命名的区别。
4.引用
4.1引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它 引用的变量共用同一块内存空间。
int main()
{
int a = 10;
int &b = a;
cout << a << endl;
cout << b << endl;
}
引用相当于个a取了另外一个名字叫b,他们共用一片空间。
注意:引用的必须和原来的变量是同一个数据类型。
4.2引用特性
1. 引用在定义时必须初始化
int main()
{
int a = 10;
int &b;
}
2. 一个变量可以有多个引用
int main()
{
int a = 10;
int &b = a;
int &c = a;
int &d = b;
}
3. 引用一旦引用一个实体,再不能引用其他实体
int main()
{
int a = 10;
int& b = a;
int c = 20;
b = c;
}
这里是给c变别名?还是把c的值给b呢?
这里可以看到是把b所在空间的值变成c,这里就不是给c取别名了。
4.3引用的应用
4.3.1指针的引用
第一个就是以前的二级指针,以前的二级指针可能不是很好理解,这个可以用引用来进行替换,来看一段代码。任何类型都引用,指针也不例外,这里叫指针的引用。
int main()
{
int a = 10;
int *p1 = &a;
int** p2 = &p1;
int*& p3 = p1;
}
这里相当于是有一块空间叫a里面存的值为10,有一个指针变量叫p1它里面存的是a的地址,然后给这个p1取一个别名叫p2。
这里的如何用引用来替换二级指针。
4.3.2引用做参数
void Swap(int *px, int *py)//传地址调用
{
int a;
a = *px;
*px = *py;
*py = a;
}
void Swap(int&rx, int& ry)//传引用
{
int a;
a = rx;
rx = ry;
ry = a;
}
void Swap(int x, int y)//传值
{
int a;
a = x;
x = y;
y = a;
}
int main()
{
int x = 10, y = 20;
cout << x <<" "<< y << endl;
Swap(&x,&y);
cout << x << " " << y << endl;
Swap(x,y);//这里传值和传引用产生了歧义,虽然他们构成重载函数。
cout << x << " " << y << endl;
}
4.3.3引用做返回值
要学习这个内容,我们首先要了解一个东西,就是传值返回。
//传值返回值
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
cout << ret << endl;
return 0;
}
我们现在看这个图来分析一下,首先main和Add函数都会生成一片空间,主函数到调用Add函数哪里进入Add函数,在Add函数生成的空间里面会生成三个小的空间存放变量a,b,c,然后做完a+b后,return返回c的值,这时c的值会存放在一个临时变量里,Add函数生成的空间会返还给内存,然后函数调用完了之后,ret的值是临时变量的,不是c的。
我们再来看一下引用返回值。
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << ret << endl;
}
这里大部分和上面的一样,我现在就只说不一样的,当add这个函数做完,然后返回的值是是c的引用(别名),然后也要一个引用来接受,这个图上写错了,应该是int &ret的,这个时候很多人就会问,那么add函数结束了不是应该会回收它自己的空间吗,哪c的引用ret不是没用了吗。这个和编译器有很大的关系,有的编译器会在函数返还空间的时候,把里面的数据清零,而有的不会,我用的vs2013就不会,这就会造成问题。下图的问题就是从新调用了add函数后,原来那片空间的数据被覆盖了,而ret指向的空间还是那一片,所以就变成了30。
然后我还想说的就是只要是调用函数就会消耗一片空间,printf和cout也一样,调用了函数后如果有值那片空间就会被覆盖。
那么怎么才能避免这种情况呢?比如用static,出了函数的作用范围都还可以有效果。
需要说明一点的是malloc生成的空间,free会数据清零。
很多就问了学了这个引用到底有什么用,在值传递的时候,感觉也没什么优势啊。当数据特别大的时候进行值传递很浪费时间和空间,因为每次调用函数都要把值进行复制,而传引用就不用了,那么我们现在来看一个例子。
#include <time.h>
struct A{ int a[10000]; };
A a;
// 值返回 -- 每次拷贝40000byte
A TestFunc1() { return a; }
// 引用返回 -- 没有拷贝
A& TestFunc2(){ return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
void TestFunc1(A a){}
void TestFunc2(A& a){}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
//TestRefAndValue();
return 0;
}
这里就可以看到传引用比传值的优势了。
1、引用传参和传返回值,有些场景下面,可以提高性能。(大对象+深拷贝对象-) | ||
2、引用传参和传返回值,输出型参数和输出型返回值。通俗点说,有些场景下面,形参的改变可以改变实参。 | ||
有些场景下面,引用返回,可以改变返回对象。--了解一下 |
4.4常引用
// 权限放大 不可以
//const int a = 10;
//int& b = a;
// 权限不变 可以
const int a = 10;
const int& b = a;
// 权限的缩小 可以
int c = 10;
const int& d = c;
这段代码很容易理解,就不用多说了。
int main()
{
double d = 1.11;
int i1 = d;
//int &i2 = d;
const int &i3 = d;
}
其中int i1=d;这段代码就是c语言的整形提升,相当于就是d的值存在了一个临时变量里,而它具有常性(不可修改),然后就把临时变量里的值又给i1,这时i1的值会变成整形。只有类型转换才能生成临时变量!!!
int &i2=d,这个代码为什么会不行呢?因为d的值存在了临时变量里,就是临时变量具有常性,它的权限就会变大,所以就不能引用。,所以就会加要句const,使它的权限变小,这个时候i3是临时变量的别名。
4.5引用和指针的区别
1. 引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型 实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
5.类联函数
5.1简单的宏
要学习了类联函数我们要知道宏,那么我们来写一段关于宏代码。写这个函数的宏时,很多人可能第一句会写成 #define Add(x,y) x+y,这句进行简单的调用是么有问题的,但调用的时候把他改成10 * Add(1, 9),那么就不行了,我们把它的表达式写出来就变成了 10*1+9 ,这个很明显就看出来了答案是19,和我们预期的结果不同,这时我们的宏就改成 #define Add(x,y) (x+y),这样还是有点问题,如果我们调用函数时传的参数是 (a&b, a | b) ,我们把表达式写出来,就是a& b+a|b,通过c语言的学习,我们知道 + 的优先级比 & 和 | 高,所以就是先进行 + 的运算,所以这样写的宏还是有点问题,这时我们就改成了 #define Add(x,y) ((x)+(y))。
int Add(int &x, int &y)
{
int ret = x + y;
return ret;
}
//#define Add(x,y) x+y
//#define Add(x,y) (x+y)
#define Add(x,y) ((x)+(y))
int main()
{
cout << Add(1, 9) << endl;//cout<<1+9<<endl;
cout << 10 * Add(1, 9) << endl;//cout<<10*1+9<<endl;
int a = 0, b = 1;
cout << Add(a&b, a | b) << endl;//cout<<a& b+a|b<<endl;
}
5.2类联函数的概念
通过写这个Add函数的宏,我们发现这个宏还是比较复杂的,所以为了弥补C语言的不足,C++就有了类联函数。为什么要学习宏和类联函数呢?
因为调用函数的时候会消耗函数栈帧,栈帧中又要保存一些寄存器,结束后又要恢复,我们看到是要消耗的。
inline int Add(int &x, int &y)
{
int ret = x + y;
return ret;
}
int main()
{
int a = 1, b = 2;
int ret = Add(a, b);
cout << ret << endl;
}
//int Add(int &x, int &y)
//{
// int ret = x + y;
// return ret;
//}
int main()
{
int a = 1, b = 2;
int ret = Add(a, b);
cout << ret << endl;
}
在Debug环境下,把上面两段代码转为汇编语言,illine直接就把函数展开,而不用inline的就要调用函数。
还有就是在Debug下面还要改一些东西,这样才能让inline有作用。
5.3类联函数的特性
1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜 使用作为内联函数。
2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等 等,编译器优化时会忽略掉内联。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会 找不到。
4.代码短小,且平凡调用的情况可以使用inline。
6.auto关键字
6.1auto的简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有 人去使用它,大家可思考下为什么? C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型 指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
int main()
{
//const int a = 10;
//auto b = a;
auto d = 20;
auto e = 1.6;
auto f = 'a';
auto g = "asda";
//cout << typeid(a).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
cout << typeid(g).name() << endl;
}
可以看到编译器的结果,auto是可以推导数据的类型。而cout << typeid(b).name() << endl;是测试变量的类型,这里不用深究,了解就行。还有就是要注意auto出的变量必须初始化。
auto还可以用来结束函数返回值。返回值如果是char和double类型的都会转化为int。
6.2auto使用规则
6.2.1. auto与指针和引用结合起来使用
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 100;
*b = 200;
c = 300;
cout << *a << endl;
cout << *b << endl;
cout << c << endl;
}
这里的auto a = &x;和auto* b = &x;其实是一个意思,都是转为指针,c变量是引用。
6.2.2. 在同一行定义多个变量
int main()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;
}
第一行是可以的,而第二行就不行了,因为auto定义同一必须是同一类型,不然编译器就会报错。
6.2.3auto不能推导的场景
1. auto不能作为函数的参数
2. auto不能直接用来声明数组
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式
7.. 基于范围的for循环
7.1基本使用
在以前的for循环就是用
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << " ";
cout<<endl;
}
现在c++引入了一个新的for循环。
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (auto&e : arr)
e *= 2;
for (auto&e : arr)
cout << e << " ";
}
可以看到运行的结果是一样的。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。
7.2注意事项
void TestFor(int a[])
{
// 范围必须是数组名
for (auto& e : a)
cout << e << endl;
}
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
TestFor(array);
}
这段代码是有问题的,因为这个for循环必须要一个范围,而这个范围的条件也有点苛刻,必须是函数名,而在函数里不行。
8.指针空值nullptr
我们来看一段代码
void f(int a1)
{
cout <<"int" << endl;
}
void f(int* a2)
{
cout << "int*" << endl;
}
int main()
{
f(0);
f(NULL);
f(nullptr);
}
为什么NULL会调用上面的函数呢?因为NULL是一个宏,而它的宏就NULL当成了0,所以调用函数的时候就会调用到错误的函数,所以说在一些场景下NULL会造成一些错误。而nullptr不会。
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
总结
历经博主几天的奋战终于把篇博客弄出来了,可能有点简陋,但是我会积极的学习,怎么才能写出优质的博客,感谢大家的支持!!!