c++入门
C++入门知识主要是解决C语言的一些不足之处,我们下面直接步入正题。
我们下面来学习c++的第一个程序:
举例:`
cpp
#include <iostream>
//std是所有c++库命名空间
using namespace std;//放到全局
int main()
{
cout << "hello world" << endl;
return 0;
}
我们学习这个程序之前要先知道一个知识点:命名空间域。
命名空间
我们在使用C语言的时候,通常会遇到变量定义冲突的情况,而C++中的命名空间域就是来解决这个问题的,下面我们来学习这个知识点:
域的概念
首先,域有以下几个分类:
1.全局域
2.局部域
3.命名空间域
4.类域
其中全局域和局部域都会影响生命周期和访问,而命名空间域只影响访问。
而类域这里追秋还没有学到,所以这里就不给大家激讲解了。
下面我们写一个域的程序要让我们更快的了解域的概念:
cpp
namespace hhd1
{
int x = 0;
}
namespace hhd2
{
int x = 1;
}
int main()
{
printf("%d\n", hhd1::x);
printf("%d\n", hhd2::x);
return 0;
}
其中namespace就是域的意思,hhd1和hhd2就是这个域的名字,我们在使用指定域中的变量的时候是用::符号来使用,::就是域作用限定符,作用是指定编译器去访问哪一个域。
而在定义域的时候进行名字的定义,那么势必会出现名字冲突的情况:
当命名空间域名字相同时,编译器会将其中的内容合并到一起。
接下来我们看下一个程序:
cpp
namespace hhd1
{
int x = 111;
}
using namespace hhd1;
int main()
{
printf("%d\n", x);
return 0;
}
讲解:using namespace hhd1 就是将命名空间域hhd1展开,使编译器可以访问到该空间中,
注:在命名空间中定义的变量依旧属于全局变量,展开后也是,编译器依旧是先访问局部然后全局,只是展开后编译器有权利去该空间域中搜索。
我们在命名空间域中还可以定义一些常见的量,比如:函数,结构体。
cpp
namespace hhd1
{
int x = 0;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
printf("%d\n",hhd1::x);
//定义结构体指针
struct hhd1::Node phead;
return 0;
}
其中我们看到函数、结构体也是可以在命名空间域中定义的,其中在主函数中还定义了结构体指针。
命名空间域的嵌套
举例:
cpp
#include <iostream>
namespace hhd
{
namespace sz
{
int x = 111;
}
namespace ls
{
int x = 120;
}
}
int main()
{
printf("%d\n", hhd::sz::x);
printf("%d\n", hhd::ls::x);
return 0;
}
我们在使用命名空间域的时候可以对域进行嵌套定义,这样就可以防止在同一域中的变量定义冲突的问题,当然,这个嵌套是可以多次嵌套的,只要你能把这个逻辑理清楚的话。
域的使用方式
1.不展开,在指定域搜索
cpp
#include <iostream>
namespace hhd
{
int x = 120;
}
int main()
{
printf("%d\n", hhd::x);
return 0;
}
2.使用using将命名空间中某个成员引入
cpp
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
3.使用using namespace 命名空间名称 引入
cpp
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
C++输入输出
输出cout
使用方法:
cpp
int main()
{
cout << "xxxxx" << endl;
return 0;
}
其中操作符<<有两种含义在C++中,
1.左移,就是我们C语言阶段所学习的关于二进制位的移动。
2.流插入,且自动识别类型
举例:
cpp
int main()
{
int x = 0;
double y = 1.2;
cout << x<< y << endl;
return 0;
}
在对cout语句使用的时候不需要像printf中一样注明类型,就可以直接打印在显示器上。
输入cin
举例:
cpp
int main()
{
int x = 0;
double y = 1.2;
cin >> x >> y ;
cout << x << y << endl;
return 0;
}
其中>>操作符在这里也是有两种作用,
1.右移,对操作数的二进制位进行操作
2.流提取,自动识别类型,不需要单独注明操作数的类型。
举例:
cpp
缺省参数
什么是缺省参数?
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
举例:
cpp
#include <iostream>
using namespace std;
void func(int i = 0)
{
cout << i << endl;
}
int main()
{
func(1);
func();
return 0;
}
运行结果:
缺省参数有以下几个分类:
全缺省
cpp
void func(int x = 10, int y = 20, int z = 30)
{
cout << "x="<< x << endl;
cout << "y="<< y << endl;
cout << "z="<< z << endl<<endl;
}
int main()
{
func(1, 2, 3);
func(1, 2);
func(1);
func();
return 0;
}
运行结果:
半缺省(部分缺省)
cpp
void func(int x, int y = 20, int z = 30)
{
cout << "x="<< x << endl;
cout << "y="<< y << endl;
cout << "z="<< z << endl<<endl;
}
int main()
{
func(1, 2, 3);
func(1, 2);
func(1);
return 0;
}
运行结果:
注:
1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现(声明和定义同时出现缺省值时编译器会无法确定哪一个才是缺省值)
3. 缺省值必须是常量或者全局变量
4. C语言不支持(编译器不支持)
函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重
载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是"谁也赢不了!",后者是"谁也赢不了!"
函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
函数重载有以下几个分类:
参数类型不同
cpp
// 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;
}
函数名相同,但是参数的类型不同。
注:要是参数相同但是返回值类型不同也是不构成重载的,因为编译器依旧无法区分。
参数个数不同
cpp
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
函数名相同,但是函数的参数数量不同。
参数顺序不同
cpp
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;
}
函数名相同,函数的参数顺序不同,这一个不同的点影响着编译器中对于函数搜索规则的变化,这个是C语言所没有的。这个点之后的文章追秋会重点讲解!
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
一个变量可以有多个别名
举例:
cpp
int main()
{
int a = 0;
//引用,b就是a的别名
int& b = a;
cout << &b << endl;
cout << &a << endl;
//可以取多次别名
int& c = a;
//也可以对别名取别名
int& d = b;
cout << &c << endl;
cout << &d << endl;
return 0;
}
运行结果:
下面来举一个例子来引出引用在函数方面的使用:
cpp
void Swap(int& c, int& d)
{
int tmp = c;
c = d;
d = tmp;
}
int main()
{
int a = 1;
int b = 5;
Swap(a, b);
cout << a << endl << b << endl;
return 0;
}
运行结果:
此时ab中的值已经被交换,那么这个时候就有人要问了,在函数形参取名的时候可以和实参一样吗?
解答:可以!
引用在定义时必须初始化
cpp
int main()
{
int a = 0;
int& b = a;
int& c;
return 0;
}
我们可以看到上述代码在编译的时候报错。
为什么引用必须初始化?
引用如果不初始化的话,那么回合赋值产生歧义。
cpp
int main()
{
int a = 0;
int b = 10;
int d;
int& c;
c = b;
d = b;
return 0;
}
上述代码c的引用和d的赋值部分根本分不清楚。
引用一旦引用一个实体,再不能引用其他实体
引用一旦确定引用一个实体之后,就不能在引用另一个实体了。
引用无法改变指向,这也是引用无法完全代替指针的一个重要的方面。
下面举一个具体场景:
在链表部分我们通常需要改变指针的指向让指针向前或向后移动,从而达到我们想要的效果,但是引用无法改变指向,因此不能够代替指针。
那么问题来了,Java、python等语言没有指针如何实现链表?
答:他们的引用可以改变指向哈哈哈
cpp
int main()
{
int a = 0;
int b = 10;
int& c = a;
&c = b;
return 0;
}
下面说几个引用在函数传参的应用:
cpp
void PushBack(struct Node*& phead, int x)
{
phead = newnode;
}
int main()
{
struct Node* plist = NULL;
return 0;
}
此处就是函数中的phead就是struct Node* plist的别名,也就是phead等价于plist;
使用场景
参数
cpp
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
1.做输出型参数;
2.对象比较大,减少拷贝,提高效率。
这两个效果,指针都可以做,只是引用更方便。
返回值
场景一:
cpp
int func()
{
int a = 1;
return a;
}
int main()
{
int ret = func();
cout << ret << endl;
return 0;
}
场景二:
cpp
int& func()
{
int a = 1;
return a;
}
int main()
{
int ret = func();
cout << ret << endl;
return 0;
}
场景三:
cpp
int& func()
{
int a = 1;
return a;
}
int main()
{
int& ret = func();
cout << ret << endl;
return 0;
}
此时在这个场景下做进一步的改动:
cpp
int& func()
{
int a = 1;
return a;
}
int& fun()
{
int b = 6;
return b;
}
int main()
{
int& ret = func();
cout << ret << endl;
fun();
cout << ret << endl;
return 0;
}
运行结果:
在以上这种场景中,只是简单调用了另一个函数,其实啥都没做,这里就会出现随机值。
结论:返回值出了函数作用域声明周期就结束了,就会被销毁,不能做引用返回。
而全局变量、静态变量、堆上空间(malloc)就可以用引用返回。
指针和引用的区别
语法:
1.引用是别名,不开空间,指针是地址,需要开空间;
2.引用必须初始化,指针可以初始化也可以不初始化;
3.引用不能改变指向,指针可以;
4.引用相对安全,没有空引用,但是有空指针,容易出现野指针,但是不容易出现也引用;5.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
有多级指针,但是没有多级引用
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
引用比指针使用起来相对更安全
底层:
在汇编层面上,没有引用,都是指针,引用编译后也会被转换成指针。
内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
内联函数其实和宏以及函数有关系,下面我们来回顾以下宏的一些知识点:
cpp
#define ADD(a,b) ((a) + (b))
int main()
{
int a = 1;
int b = 2;
int c = ADD(a, b);
cout << c << endl;
return 0;
}
以上代码就是宏的使用的一个简单代码,其中也会出现几个问题:为什么a、b要单独加括号将他们单独放在一起:
解答:因为可能原始参数可能传的不仅仅是一个参数,而是一个表达式,下面我们来看:
cpp
#define ADD(a,b) ((a) + (b))
int main()
{
int a = 1;
int b = 2;
int c = ADD(a + b , a - b);
cout << c << endl;
return 0;
}
上面这个例子就很好的说明了宏的一个正确使用方式。相比较于函数,宏是不需要建立栈帧的,也就减少了栈帧空间的开销,当然宏也不是完美的,宏也有缺点:
宏的缺点:
1、语法复杂,坑很多,不容易控制;
2、不能调试;
3、没有类型安全的检查。
而内联函数就将函数和宏的优势结合在了一起:
我们知道:函数在传递参数的时候,如果参数是表达式,那么会将表达式的值算好在传值,而宏是不需要单独建立栈帧,编译器在编译的时候会自动将宏的内容进行替换,这里内联函数就将这一点完美的应用到了:
cpp
inline int ADD(int x, int y)
{
return x + y;
}
int main()
{
int a = 1;
int b = 2;
int c = ADD(a + b , a - b);
cout << c << endl;
return 0;
}
这里的运算不建立栈帧,直接在原地进行替换且参数是运算好的。
内联函数特性
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。下面将一个例子来说明这个问题:
下面将一个内联函数一个在实践当中的一个小应用的地方,
当一个函数的声明和定义没有分离的时候,函数在运行的时候会出现报错:
想要解决这个问题我们有以下几个解决方法:
1.声明和定义分离。
2.static修饰函数
3.inline修饰
用inline修饰的函数直接在.h文件中定义,作用机制和static类似,因为inline修饰对象较小时,编译时会在使用的地方展开,因此不会再符号表中出现,也不会出现链接错误的问题。当然,目标函数不能过大,不然inline会不起作用的。
auto关键字
识别类型
cpp
int main()
{
int aw = Add(1, 2);
int a = 1;
auto b = a;
auto c = &a;
auto* d = &a;
return 0;
}
在上述的情况下,定义一个新的数据就不用特意声明他的类型,系统会自动识别数据类型并完成定义。
auto关键字也可以定义函数类型:
当然在这里自动识别类型定义显得价值不大,但是在以后类型很复杂的情况下,auto函数的价值就会显示出来。
auto不能推导的场景
不能作函数的参数
编译器允许auto作为返回类型但不能作为参数。
不能用来声明数组类型
cpp
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
基于范围的for循环(C++11)
范围for的语法
cpp
int main()
{
int a[] = { 1,2,3,4,5 };
for (auto e : a)
{
cout << e << endl;
}
return 0;
}
这其实就是一种新的for循环的使用方式,auto会将数组a中的值依次放到e中,然后我们打印的结果就是:
范围for的使用条件
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
2. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)
指针空值nullptr(C++11)
这个点其实是在弥补C++之前的缺陷:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
cpp
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
注意:
*1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void )0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。**