文章目录
- 前言
- 一、C++历史及发展
- 1.C++是什么
- 2.C++历史
- 二、开始C++
- 1.基础类型
- 1.第一个简单的C++程序
- 2.命名空间
- 1.命名空间的介绍
- 2.命名空间的使用
- 3.命名空间的using声明与using指示
- 3.初识输入输出操作
- 4.引用
- 1.引用概念
- 2.引用的使用
- 1.引用做参数
- 2.引用做返回值
- 3.引用和指针的区别
- 4.const的引用
- 5.auto关键字
- 1.auto简介
- 2.复合类型,常量和auto
- 1.auto与指针和引用结合使用
- 2.语句
- 1.范围for
- 3.函数
- 1.缺省参数的函数
- 1.缺省参数的概念
- 2.缺省参数的分类
- 1.全缺省参数
- 2.半缺省参数
- 2.函数的重载
- 1.函数重载的概念
- 2.调用重载的函数
- 3.内敛函数
- 4.其他
- 1.nullptr
前言
C++是在C的基础之上,容纳进去了面向对象编程思想,并且增加了许多有用的库。熟悉C语言对C++学习有很大的帮助,C++是对C语言的补充和对C语言进行优化。本章我们学习一点C++的基础内容,先浅浅的了解一下C++。
一、C++历史及发展
了解一门语言,我们要先看一下它的发展历史和出现的原因,方便我们更好的了解语言适合解决什么样的问题。
1.C++是什么
在20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,为了解决C语言对于复杂的问题,规模较大的程序,需要高度的抽象和建模时的困境。
在1982年,Bjarne Stroustrup(本贾尼)博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的语言。为了表达该语言与C语言的关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
2.C++历史
1979年,贝尔实验室的Bjarne Stroustrup(本贾尼)等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。现在公司主流使用还是C++98和C++11。
二、开始C++
1.基础类型
1.第一个简单的C++程序
#include<iostream>
using namespace std;
int main()
{int a = 0;int b = 0;//从键盘上获得值,相当于C语言的scanf函数cin >> a >> b;//把结果输出到屏幕,相当于C语言的printf函数cout << a + b << endl;return 0;
}
相信大部分课本的代码案例都是如此。让我们看一下运行的结果吧.
这个简单的小程序就实现了,但是在C++Primer中是下面的写法:
#include<iostream>
int main()
{int a = 0;int b = 0;std::cin >> a >> b;std::cout << a + b << std::endl;return 0;
}
结果和上面一模一样,那么上面的using namespace std和std::分别是什么呢?为什么可以有两种写法呢?
2.命名空间
针对上面的问题,我们来介绍一下C++中的命名空间。
1.命名空间的介绍
因为变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
对比上面的代码我们可以看出使用了未加using namespace std在实现代码中多了一些东西,这是因为我们用到的库函数基本都属于命名空间std,例如std::cin表示标准输入中获取内容。此处使用的是作用域操作符(::),意思是编译器应该从操作符左侧的作用域来寻找右侧的名字。因此std::cin的意思是使用命名空间std的名字cin
2.命名空间的使用
当我们多个文件使用的全局变量,函数或者结构体使用的名字相同时,这时间就会产生命名冲突,在C语言中是没半法很好的解决这些问题的。如:
那么在C++中如何解决这些问题的呢,我们引入了命名空间这个概念。
namespace关键字,后面跟命名空间的名字,然后加一对{},{}中即为命名空间的成员。
加入我们的命名空间就可以解决这样的问题了命名空间也是可以嵌套的。
3.命名空间的using声明与using指示
我们先看什么是using声明与using指示
区别:
一条using声明语句一次只引入命名空间的一个成员,它使得我们很清楚地知道程序中所用的到底是哪个名字。一条using声明语句可以出现在全局作用域,局部作用域,命名空间作用域以及类的作用域,在类的作用域中,这样的声明语句只能指向基类成员。
using指示中,我们无法控制哪些名字是可见的,因为所有名字都是可见的。且using指示不可以出现在类中。
在using指示中,以关键字using开始,后面跟namespace关键字以及命名空间的名字,如果这里的名字不是已经定义好的命名空间名字,程序将会发生错误。
命名空间只会影响使用,不会影响生命周期。
更加详细的可以看C++Primer。
3.初识输入输出操作
C++并未定义任何输入输出(IO)语句,而是包含了一个全面的标准库来提供IO机制。我们输入输出使用了iostream库,iostream库包含两个基础类型istream和ostream,分别表示输入流和输出流
标准库定义了4个IO对象,为了处理输入,我们使用了一个cin的istream类型的对象,这个对象也被称为标准输入。对于输出,我们使用了一个cou的ostream类型的对象,此对象也被称为标准输出。标准库还定义了其他两个ostream对象,分别为cerr和clog,我们一般用cerr来输出警告和错误信息,因此cerr也叫标准错误。而clog用来输出程序运行时的一般性信息。
#include<iostream>
using namespace std;
int main()
{int a = 0;int b = 0;cin >> a >> b;//标准输入cout << a + b <<endl;//标准输出return 0;
}
上面一个简单的相加程序就用到了们标准输入和标准输出。
<<(输出运算符):<<运算符接受两个运算对象:左侧的运算对象必须是一个ostream对象,右侧的对象是要打印的值。此运算符将给定的值写入到给定的ostream对象中。
>>(输入运算符):>>与输出运算符相似,它接受一个istream作为其左侧运算对象,接收一个对象作为右侧运算对象。此运算符将给定的值写入到给定的ostream对象中。
cin >> a;cin >> b;cin >> a >> b;//效果和上面等价cout << a ;cout << b ;cout << a << b;//效果和上面等价
4.引用
1.引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
int main()
{//类型& 引用变量名(对象名) = 引用实体int a = 10;int& sa = a;//sa指向a(是a的另一个名字)int& saa;//报错:引用必须被初始话return 0;
}
定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用,一旦初始话完成,引用将和它的初始值对象一直绑定在一起。因为无法更改绑定对象,所以引用必须初始化!!!
int main()
{int a = 10;int& sa = a;a++;//对a进行++cout <<"sa = " << sa << endl;//sa的值也会随着发生改变sa++;//对sa进行++cout <<" a = " << a << endl;//a的值也会随着发生改变return 0;
}
下面我们看一些引用的例子:
int main()
{int val = 10;int rval1 = val;//把val的值赋给rval1int &rval2 = val;//rval2是val的引用int& rval3 = rval2;//rval3是rval2的引用,此时也是val的引用int& rval4 = 10;//错误:引用类型的初始值必须是一个对象double& rval5 = val;//错误:此处引用的初始值类型必须为double类型return 0;
}
在引用中,引用只能绑定在对象中,而不能与字面值或某个表达式的计算结果绑定到一起。但也有例外情况。原因也会在下面演示。
2.引用的使用
1.引用做参数
void Swap(int* a, int* b)//指针类型接收参数
{int tmp = *a;*a = *b;*b = tmp;
}
void SWAP(int &sa, int &sb)//引用类型接收参数,此时的sa就是main函数中的a,sb是main函数中的b
{int tmp = sa;sa = sb;sb = tmp;
}
int main()
{int a = 10;int b = 20;Swap(&a, &b);//指针类型的传参printf("a = %d b = %d\n", a, b);SWAP(a, b);//引用类型传参printf("a = %d b = %d\n", a, b);return 0;
}
2.引用做返回值
int& ADD(int a, int b)
{static int c = 0;c = a + b;return c;
}
int& add(int a, int b)
{int c = 0;c = a + b;return c;
}
int main()
{int a = 10;int b = 20;int& c1 = ADD(a, b);cout << c1 << endl;cout << "ADD(a, b) val is:" << c1 << endl;int& c2 = add(a, b);cout << c2 << endl;cout <<"add(a, b) val is:" << c2 << endl;return 0;
}
上面的代码有什么结果呢?
对比可以发现,当引用做返回值时如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。因为函数调用创建栈帧会使用该片空间。
3.引用和指针的区别
int main()
{int val = 10;int vbl = 20;int &rval = val;//rval是val的引用int* pa = &val;//pa是指向val的指针(*pa)++;cout << val << endl;rval++;cout << val << endl;pa = &vbl;//pa现在指向vblreturn 0;
}
运行结果:
观察汇编代码:
我们发现引用的汇编指令和指针的汇编指令相同,引用的底层逻辑就是指针。
引用和指针的区别:
- 引用概念上定义一个变量的别名,指针存储一个变量地址
- 引用在定义时必须初始化,引用不可以为NULL指针可以为NULL
- 引用初始化后就不可以更改,而指针随时更改指向对象。
- 在sizeof中引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全。
4.const的引用
我们可以把引用绑定到const对象上,就像绑定到其他的对象上一样,我们称之为对常量的引用,与普通引用不同的是,对常量的的引用不能被作用修改它所绑定的对象。
int main()
{const int a = 10;const int& ra1 = a;//引用及其对象都是常量int& ra1 = a;//错误,试图让一个非常量引用指向一个常量对象
}
在我们上面看到的引用类型必须与其所引用的对象类型一致,但有两个例外:
一是在初始化常量引用时允许任意表达式作为初始值,只要该表达式的结果可以转化为引用类型即可,尤其允许为一个常量引用绑定非常量的对象,字面值,甚至是一个表达式。
int main()
{int a = 10;const int& sa1 = a;//允许将const int& 绑定到一个普通int对象上const int& sa2 = 10;//sa2是常量引用,因为sa2不可以改变,所以可以绑定到常量上const int& sa3 = sa1 * 2;//sa3是常量引用int &sa4 = sa1 * 2;//sa4是一个普通引用,不可以绑定到常量上return 0;
}
我们下面来看由duoble绑定int类型为什么不可以
int main()
{double val = 3.14;const int& sval1 = val;//正确int& sval2 = val;//错误return 0;
}
我们来分析一下原因:
类型转化都会产生临时变量,临时变量具有常属性,所以这也是为什么上面的绑定不成功的原因,我们需要用常引用来进行绑定。
5.auto关键字
1.auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,于是C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。显然,auto定义的变量必须有初始值。
int Testauto()
{return 0;
}
int main()
{int a = 10;auto b = a;auto c = 'a';auto d = Testauto();cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;return 0;
}
2.复合类型,常量和auto
编译器推断出来的auto类型有时间和初始值的类型并不完全一样,编译器会适当的改变结果类型使其更符合初始化规则
1.auto与指针和引用结合使用
使用引用其实就是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值
int main()
{int a = 10;int& b = a;auto c = b;cout << typeid(c).name() << endl;return 0;
}
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;return 0;
}
从上面我们可以看出用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
要在一条语句中定义多个变量(*和&只从属于某个声明符,而非基本数据类型的一部分),初始值必须是同一类型。
int main()
{auto a = 1, b = 2;auto c = 1, d = 2.0;// 该行代码会编译失败,因为c和d的初始化表达式类型不同return 0;
}
注意:auto不能作为函数的参数, auto也不可直接声明数组。
2.语句
1.范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
//语法形式
for (declaration : expression)
{statement;
}
expression:表示的必须是一个序列,如数组,vector或者string类型等类型的对象,这些类型的共同特点就是拥有能返回迭代器的begin和end成员(后面会提迭代器)
declaration:定义一个变量,序列中的每个元素都可以转换为该变量的类型。确保这些类型的最简单办法就是使用auto。
statement:循环语句。
int main()
{int tmp[] = { 1,2,3,4,5,6,7,8,9 };for (auto i : tmp){cout << i << " ";}return 0;
}
这就是一个简单范围for
思考一下下面的代码中数组的内容会不会改变呢?
int main()
{int tmp[] = { 1,2,3,4,5,6,7,8,9 };for (auto i : tmp){i *= 2;cout << i << " ";}cout << endl;for (auto i : tmp){cout << i << " ";}return 0;
}
对比发现,我们对定义变量的改变并不会影响我们数组的内容。因为我们对定义的变量是对序列中元素的拷贝。对变量的改动并不会影响我们序列中的元素。
我们再来看下面的代码:
int main()
{int tmp[] = { 1,2,3,4,5,6,7,8,9 };for (auto i : tmp){i *= 2;cout << i << " ";}cout << endl;for (auto i : tmp){cout << i << " ";}return 0;
}
此时我们对变量的更改影响了我们数组中的元素,因为我们第一次范围for中的定义的变量为引用,它是序列元素的别名,对这个变量的改变就相当于对数组内容的改变。
判断下面的代码是否是正确的呢?
void Testfor(int tmp[])
{for (auto& i : tmp){cout << i << " ";}
}
int main()
{int tmp[] = { 1,2,3,4,5,6,7,8,9 };return 0;
}
答案是不正确的,范围for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
我们看到编译器也允许这样使用,在上面代码中数组传参,退化为了指针,此时用范围for找不begin和end的位置。
3.函数
1.缺省参数的函数
1.缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
如:
void Cout(int a = 0)
{cout << a << endl;
}
int main()
{int a = 10;Cout();Cout(a);return 0;
}
当我们没有像这个函数传入参数时,函数使用的默认值0,当我们传入参数时,函数使用我们所传递的参数。
2.缺省参数的分类
1.全缺省参数
void Cout(int a = 1, int b = 2, int c = 3)
{cout<<"a = "<<a<<endl;cout<<"b = "<<b<<endl;cout<<"c = "<<c<<endl;cout << endl;
}
上面这个就是全缺省参数的函数,所有的形参都有默认值。
下面我们来测试一下函数:
int main()
{int a = 10;int b = 20;int c = 30;Cout();Cout(a);Cout(a,b);Cout(a,b,c);Cout(c);return 0;
}
我们可以发现当我们传入的参数小于所需的个数时,形参会从左向右依次接收。
2.半缺省参数
void Cout(int a, int b = 2, int c = 3)
{cout<<"a = "<<a<<endl;cout<<"b = "<<b<<endl;cout<<"c = "<<c<<endl;cout << endl;
}
此时这个函数就是半缺省参数。
注意:半缺省参数必须从右往左依次来给出,不能间隔着给,且缺省参数不能在函数声明和定义中同时出现。一旦某个形参被赋予了默认值,那么他后面的所有形参都必须有默认值。
2.函数的重载
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类顺序)不同。常用来处理实现功能类似数据类型不同的问题。
1.函数重载的概念
在同一作用域内的几个函数名字完全相同但形参列表不同,我们称之为重载函数
int Add(int a, int b)
{return a + b;
}
double Add(double a, double b)
{return a + b;
}
int Add(int a, double b)
{return a + b;
}
int main()
{int a = 10, b = 20;double c = 10.125, d = 20.25;cout << Add(a, b) << endl;cout << Add(c, d) << endl;return 0;
}
如上述代码中的Add就构成了重载
构成函数的重载必须是函数参数不相同(如个数,类型等)
2.调用重载的函数
当我们定义了一组重载函数后,我们需要合理的实参来调用他们,函数匹配是一个过程,函数匹配也叫重载确定,用编译器决定调用哪一个函数。
此时有三种情况:
1.编译器找到一个与实参最佳辟匹配的函数,并生成调用该函数。
2.找不到任何一个函数与调用的实参相匹配,此时编译器发出无匹配的错误信息。
3.有多于一个函数可以进行匹配,但每个都不是最佳选择,此时也发生错误,称为二义性调用。
如上图(出自《程序员的自我修养》p89),是vs对函数重载在编译器的名称。
3.内敛函数
我们可以将函数指定为内敛函数(inline),编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
inline int Add(int a, int b)
{return a + b;
}
int main()
{int a = 10, b = 20;int c = Add(a, b);return 0;
}
上面在编译过程展开类似下面的形式:
int c = a + b;//直接对函数进行展开
内敛说明只是向编译器发送一个请求,编译器可以忽略这个请求。
一般来说,内敛机制用于优化规模小,流程直接,调用频繁的函数,很多编译器都不支持内敛递归函数。
nline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
4.其他
1.nullptr
nullptr是一种特殊类型的字面值。
在过去的程序中用的名为NULL的预处理变量来给指针赋值,这个变量在头文件cstdilb中定义,它的值为0,因此使用NULL初始化指针和用0初始化指针是一样的。在C++中最好使用nullptr,同时尽量避免使用NULL。
void f(int)
{cout << "f(int)" << endl;
}
void f(int*)
{cout << "f(int*)" << endl;
}
int main()
{f(0);f(NULL);return 0;
}
我们可以发现当我们传NULL时并没有走指针的函数,所以当我们传指针类型时,避免使用NULL