大家又是好久不见,在和C语言告一段落之后,今天也该开启我们c++的学习了,作者自己将c++的学习分为两个阶段,c++初阶和c++高阶,c++初阶就是学习c++中最基本的知识。
话不多说,我们现在就开始今天的学习吧!
目录
1. c++的第一个程序
我们首先来看一下下面的代码:
// test.cpp#include<stdio.h>int main(){printf("hello world\n");return 0;}
是不是感觉非常眼熟呢,该代码是可以在c++的环境中运行的,说明了c++兼容C语言这样一个事实。
但既然我们是在学习c++,最好要放下在学习C语言的时候的固有观念,下面是上面代码以c++方式的写法:
#include<iostream>using namespace std;int main(){cout << "hello world\n" << endl;return 0;}
这⾥的std cout等我们都看不懂,没关系,下⾯我们会依次讲解。
2. c++中的命名空间
2.1 namespace的价值
#include<stdio.h>#include <stdlib.h>int rand = 10;int main(){// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”printf("%d\n", rand);return 0;}
我们知道,rand函数在C语言中是获得一个随机数的函数,我们同时定义了一个全局变量rand,当我们在使用这个变量rand的时候就会报错,因为编译器不知道我们使用的到底是哪一个rand。
而C++引⼊namespace就是为了更好的解决这样的问题。
大家看见了namespace的好处,现在想不想知道namespace是怎样使用的,没关系马上就教会大家。
2.2 namespace的定义
我们先来看一看namespace定义的规则:
- 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
- namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下⾯的rand不在冲突了。
- C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的声明周期,命名空间域和类域不影响变量声明周期。
- namespace只能定义在全局,当然他还可以嵌套定义。
- 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
- C++标准库都放在⼀个叫std(standard)的命名空间中。
了解了这些规则以后,我们尝试着自己写一个命名空间好了:
namespace stu1
{
char stuname[20] = { "zhang san" };
int Math = 90;
int English = 88;
int sum(int a, int b)
{
return a + b;
}
}
我现在已经写好了一个命名空间namespace,stu1是这个namespace的名字,代表一个学生,namespace中的变量为这个学生的姓名和他的数学英语的两门的分数,namespace中还包含了一个函数用来求学生的数学和英语的总分。这样一个简单的namespace就写好了。
namespace student
{
namespace stu1
{
char stuname[20] = { "zhang san" };
int Math = 90;
int English = 88;
}namespace stu2
{
char stuname[20] = { "li si" };
int Math = 89;
int English = 92;
}int sum(int a, int b)
{
return a + b;
}
}
我们再来看这一段代码,我们在student这个namespace中又定义了两个namespace,说明namespace是可以嵌套的,而且由于这两个小的命名空间中都有相同的函数,所以我把这个函数拿到了大的命名空间里。
namespace stu1
{
char stuname[20] = {"zhang san"}
}
namespace stu1
{
int Math = 90;
int English = 88;
}
当两个命名空间名相同时,会认为是同一个namespace,不会重复定义。
2.3 namespace的使用
讲完了namespace的定义,接下来我们来学习namespace的使用。
- 指定命名空间访问,项⽬中推荐这种⽅式。
- using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
- 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
#include<stdio.h>
namespace bit
{
int a = 0;
int b = 1;
}
int main()
{
// 编译报错:error C2065: “a”: 未声明的标识符
printf("%d\n", a);
return 0;
}
由此可见,我们在namespace中定义的变量不能直接使用,那到底要怎么使用呢?
有以下两种使用方法:
// 指定命名空间访问
int main()
{
printf("%d\n", N::a);
return 0;
}
// using将命名空间中某个成员展开
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
一种是在使用时将namespace中的成员展开,另一种方法是提前将namespace中的成员展开,这样就能像上文一样直接使用了。
还有一种展开方法:
// 展开命名空间中全部成员
using namespce N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
就是将命名空间namespace中的所有成员全部展开,但这样可能也会引发一些问题,比如最常见的重名问题,所以不建议全部展开。
3. c++中的输入输出
说完了namespace之后,我们来学习一下c++中的输入输出,<iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输 ⼊流。std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。<<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
#include<iostream>
using namespace std;
int main()
{
int a = 0;
double b = 1.0;
char c = 'a';
cout << a << b << c << endl;
std::cout << a << b << c << std::endl;
return 0;
}
因为整体展开了std这个命名空间,所以cout和endl可以直接使用也可以再单独展开使用。
在C语言中,我们对浮点数经常会通过改变输出小数点后数字个数来修改精度,那么在c++中应该怎么修改小数点后数字个数呢?
在c++中,我们会使用一个函数来达到修改小数点后数字个数的目的:
#include<iostream>
#include<iomanip>//必要头文件
using namespace std;
int main()
{
double sum;
sum=4.56789;
cout<<"方法一:保留两位小数"<<endl;
//第一种方法也是最常用的方法
cout<<fixed<<setprecision(2)<<sum<<endl;
cout<<"方法二:保留三位小数"<<endl;
cout.setf(ios::fixed);
cout<<setprecision(3)<<sum<<endl;
cout<<"方法三:保留四位小数"<<endl;
cout<<setiosflags(ios::fixed)<<setprecision(4)<<sum<<endl;
cout<<"方法四:保留六位小数"<<endl;
cout<<setprecision(6)<<sum<<endl;
return 0;
}
我们知道有一些数字会以科学计数法来表示,此时若直接修改精度会出现错误,这时就要使用到fixed函数了,其中fixed函数是用来消除科学计数法的,那么对于我们知道的一些普通的浮点数,可以直接使用方法四来进行修改精度。
4. 缺省参数
- 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左
- 依次连续缺省,不能间隔跳跃给缺省值。带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
#include<iostream>
using namespace std;
void test1(int a = 10)
{
cout << a << endl;
}
void test2(int a = 10, int b = 20)
{
cout << a + b << endl;
}
void test3(int a, int b = 20)
{
cout << a + b << endl;
}
int main()
{
int a = 3, b = 5;
test1();
test1(a);
test2();
test2(a, b);
test3(a, b);
return 0;
}
test1和test2都是全缺省函数,而test3是半缺省函数,有一点要多说的是,半缺省函数在调用时不能不传参数,会报错。
5. 函数重载
- 参数类型不同
- 参数个数不同
- 参数顺序不同
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
相反,返回值不能作为函数重载的条件,因为假如两个函数只有返回值类型不同的话,那么在调用函数的时候是无法区分这两个函数的。
6. 引用
6.1 引⽤的概念和定义
在c++中,有很多运算符是有不同的意思的,会复⽤C语⾔的⼀些符号,⽐如前⾯的<< 和 >>,在C语言中代表的是二进制的运算,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。
类型& 引⽤别名 = 引⽤对象;int& b = a;
比如说在这一条代码中b就是a的引用,b是a的别名。
6.2 引⽤的特性
- 引⽤在定义时必须初始化
- ⼀个变量可以有多个引⽤
- 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
比如说上文中b在引用a的时候,a必须是初始化的,因为一个变量在声明的时候是不被分配空间和地址的,只有初始化后才会有空间和地址。
而且一个变量可以有很多引用,但一个引用只能引用一个变量,且无法被修改。
6.3 引⽤的使⽤
- 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
- 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。 引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
- 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。
- ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
#include<iostream>
using namespace std;
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
这就是引用在传参中的用法,以后我们会进一步学习。
6.4 const引⽤
我们在C语言中学到过,const修饰一个变量后这个变量就无法被修改了,在c++中也是一样。
int main()
{
const int a = 10;
// 编译报错:error C2440: “初始化”: ⽆法从“const int”转换为“int &”
// 这⾥的引⽤是对a访问权限的放⼤
//int& ra = a;
// 这样才可以
const int& ra = a;
// 编译报错:error C3892: “ra”: 不能给常量赋值
//ra++;
// 这⾥的引⽤是对b访问权限的缩⼩
int b = 20;
const int& rb = b;
// 编译报错:error C3892: “rb”: 不能给常量赋值
//rb++;
return 0;
}
6.5 指针和引⽤的关系
- 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
- 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
- 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
- sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
- 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
7. inline
- ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率。
- inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
- C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数。
- vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅。
- inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
可以发现,inline函数和C语言中学过的宏很像,但是inline函数相比宏更简便,我们想要写一个宏需要考虑宏在使用的时候的代码环境,可能会影响到宏的结果,但是inline函数就不用考虑这么多。另外,inline还有好处就是当代码量小的时候会以inline函数进行展开,而代码量长时会以普通函数一样正常调用。
8. nullptr
在C语言中我们认为NULL是空指针(被定义为⽆类型指针(void*)的常量),而在c++中,NULL被认为是0,而在c++中引用了nullptr作为空指针,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
// 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。
f(NULL);
f((int*)NULL);
// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
// f((void*)NULL);
f(nullptr);
return 0;
}
由于NULL在c++中被认为是0,所以调用函数会输出 f(int x) ,且NULL为0,当强制转为指针时会报错,而对于nullptr来说,可以转为任意类型的指针,所以会输出 f(int* ptr) 。
大家只要记住,在c++中不使用NULL作为空指针就好了。
我们今天的学习就结束啦,期待下次再见!