C++入门基础
关键字namespace命名空间
解决变量命名冲突的问题
将全局作用域分成若干个部分 每个命名空间就是一个作用域
注意:命名空间不能在函数、类中等局部空间中定义
C语言中的缺陷
//在C语言中 没有很好的解决命名冲突的问题
#include<stdio.h>
#include<stdlib.h>
int rand=0;//库函数中定义了rand函数,此时自己重定义,会发生命名冲突
int main()
{
printf("%d\n",rand);
return 0;
}
C++中的
//C++中引入namespace解决
#include<stdio.h>
#include<stdlib.h>
namespace s
{
int rand=0;
}
int main()
{
printf("%d\n",rand);
return 0;
}
打印出来的并不是0 而是一个很大的数 那是rand函数地址的十进制形式
因为rand=0 是在s命名空间中的 而直接打印的是rand函数
namespace s
{
int b=1;
}
int a=0;
int b=0;
int main()
{
int a=1;
printf("%d\n",a);//输出的是1 因为在函数体内 局部变量优先
printf("%d\n",::a);//::左侧为空 默认是全局命名空间
printf("%d\n",s::b);//s域中的b变量
return 0;
}
全局命名空间
除了类,函数,其他命名空间以外 也就是默认的命名空间
命名空间可以定义变量、函数、类型
namespace s{
int a=0;
void f()
{
printf("good job!!");
}
struct ListNode{
int val;
struct ListNode* next;
};
}
命名空间的嵌套
namespace Na
{
int a=0;
int b=0;
int add(int left,int right)
{
return left+right;
}
namespace Nb
{
int c=0;
int sub(int left,int right)
{
return left-right;
}
}
}
命名空间可以不连续
同一工程中 允许存在多个相同名称的命名空间 编译器最后会合成同一个命名空间
//某一个文件中
namespace s
{
int a=0;
struct Node
{
int val;
struct Node* next;
};
}
//另一个文件中 或 同一文件中
namespace s
{
int b=0;
struct Node
{
int val;
struct Node* next;
};
}
由于命名空间名称相同 二者进行合并
但是二者中又存在着 相同的结构体命名 导致命名重复问题出现
所以应进行命名空间的嵌套
//某一个文件中
namespace s
{
int a=0;//还是一个全局变量,命名空间不影响生命周期
namespace a
{
struct Node
{
int val;
struct Node* next;
}
}
}
//另一个文件中 或者 同一文件中
namespace s
{
int b=0;
namespace b
{
struct Node
{
int val;
struct Node* next;
}
}
}
int main()
{
struct s::b::Node n1;//s::b::Node n1 也是可以的 c++中可以省略struct
n1.val=2;
s::a::Node n2;
return 0;
}
命名空间的使用
加命名空间名称和作用域限定符
namespace s{
int a=1;
int b=2;
}
int main()
{
printf("%d\n",s::a);
return 0;
}
使用using将 命名空间中的成员引入
namespace s{
int a=1;
int b=2;
}
using s::a;
int main()
{
printf("%d",a);
return 0;
}
使用using namespace 命名空间名称引入
namespace s{
int a=1;
int b=2;
}
using namespace s;
int main()
{
printf("%d",a);
return 0;
}
所以说常见的
using namespace std;
std是C++的标准库
这句话就是将标准库引入进来
如果不引入这句话 就得使用 using std::等等
例如using std::cout;
输入和输出
#include<iostream>
using namespace std;
int main()
{
int a;
double b;
cin>> a >>b;// >> 输入运算符(流提取运算符)
cout<<a <<b<<endl;// << 输出运算符(流插入运算符)
cout<<"helle world"<<endl;//endl是操纵符 清空缓冲区 保证输出都在输出流 而不是在等待写入流 也就是说想输出的都可以输出
cout<<"hello world"<<'\n';
return 0;
}
相对比C语言
C++可以多个同时输入输出 并且自动识别数据类型
但是按某种格式输出很麻烦 可以使用C语言 进行格式控制 (C++兼容C语言)
缺省参数(默认参数)
概念
声明或定义时为函数指定一个默认值
在调用该函数时 如果没有传入对应的实参 则采用该默认值 否则使用传入的实参
(备胎的眼泪 呜呜)
void Test(int a=10)
{
cout<<a<<endl;
}
int main()
{
Test();//使用默认值
Test(1);//使用传入的实参
return 0;
}
全缺省参数
字面意思 每个参数都有默认值
void Test(int a=10,int b=20,int c=30)
{
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
int main()
{
Test();//a=10,b=20,c=30
Test(1);//a=1,b=20,c=30
Test(1,2);//a=1,b=2,c=30
Test(1,2,3);//a=1,b=2,c=3
return 0;
}
半缺省参数
半缺省参数并不代表 有一半的参数有默认值 而是说 有部分参数有默认值
注意:半缺省参数必须从右向左给出,不能隔着给
因为传参从左往右传 否则传参带有歧义
void Test(int a,int b=20,int c=30)
{
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
int main()
{
Test(1);//a=1,b=20,c=30
Test(1,2);//a=1,b=2,c=30
Test(1,2,3);//a=1,b=2,c=3
return 0;
}
struct Stack
{
int *a;
int size;
int capacity;
};
void StackInit (struct Stack* ps,int n=4)
{
assert(ps);
pa->a=(int*)malloc(sizeof(int)*n);
ps->size=0;
ps->capacity=n;
}
int main()
{
struct Stack st;
//StackInit(&st);使用默认值
StackInit(&st,20);
return 0;
}
缺省参数在声明和定义最好不要同时出现
声明和定义都给默认参数
//test.h
void Test(int i=10)
//test.c
void Test(int i=20)
{
}
因为声明和定义可能 默认参数写的不同
编译器无法判断该使用哪一个默认参数
声明给缺省参数 定义不给缺省参数
//test.h
void Test(int a=10);
//test.c
void Test(int a)
{
}
int main()
{
Test();
Test(1);
return 0;
}
编译通过
声明不给默认参数 定义给默认参数
//test.h
void Test(int a);
//test.c
void Test(int a=10)
{
}
int main()
{
Test();//编译失败
Test(1);
return 0;
}
编译失败
原因在于,声明在头文件中,编译阶段,头文件就展开了,而声明未加默认参数,自然编译错误,查看函数的定义是在链接阶段
总结
可以进行声明和定义分离的函数(不是类的成员函数)
声明写默认参数 定义不要写
函数重载
概念
在同一作用域允许声明名称相同的函数,参数不同(参数个数,参数类型,顺序不同)
其中 顺序不同 指的是参数类型的顺序
void a(int i,double j)
{
}
void a(double j,int i)
{
}
光是参数名不同 不构成重载
void a( int i)
{
}
void a( int j)
{
}
光是返回值类型不同 也不构成重载
long add(int left , longt right)
{
return left+right;
}
int add(int left , longt right)
{
return left+right;
}
int f()
{
return 1;
}
int f(int i=1, int j=1)
{
return 2;
}
int main()
{
f();//歧义 不知道是调用 全缺省函数 还是 无参函数
}
原理
引用
概念
引用不是新定义一个变量 而是给已经存在的变量取别称
编译器 不会给引用变量开辟新的内存空间 引用变量 和被引用变量 公用一块内存空间
正如孙悟空 又叫孙行者 齐天大圣
这三者 说的都是他一人
格式
类型&引用变量名(对象名)=引用实体(被引用变量)
int a=10;
int& b=a;//b和a得是同一类型
特性
- 引用在定义时必须初始化
int& b;//错误
- 一个变量可以有多个引用
int a=10;
int& b=a;
int& c=a;
int& d=b;
- 一旦引用一个实体,不能再改变
int a=10;
int b=20;
int& c=a;
int& c=b;//错误
c=b;//将b的值赋给 c也就是a
常引用
使用引用的原则:对于原引用变量,读写权限只能不变或者缩小,不能放大
const int x=10;
int& y=20;//权限放大 不行
int a=5;
const int& b=a;//权限缩小
const int c=2;
const int& d=c;//权限不变
隐式类型转换时候也需要使用常引用 来控制权限
const int& f=d;
在d进行隐式类型转换时,会创建一个临时变量,将double类型转换成int类型(不是单纯的截断,因为浮点型和整数型的存储机制不同)存储到临时变量中
临时变量具有常型,也就说临时变量是被const修饰的,这也就能说明为什么一定要加const
int &f= (int)d;
再看地址
f和d的地址不同
f引用的变量并不是d, 而是一个int类型的临时变量。
按道理说临时变量时使用完立刻销毁的
但是如果这个临时变量被用来初始化一个引用的话,那这个临时变量的生命周期就会被延长,直到引用被销毁。
使用场景
做参数
void Swap(int x,int y)//不改变实参
{
int tmp=x;
x=y;
y=tmp;
}
void Swap(int*x,int*y)//改变实参本身
{
int tmp=*a;
*a=*b;
*b=tmp;
}
void Swap(int&x,int&y)//改变实参参本身
{
int tmp=x;
x=y;
y=tmp;
}
意义:
输出型参数(输出类型的参数)
这是C语言中一道二叉树前序遍历的题
returnSize是int型指针 来记录有效的节点个数
函数内部需要使用解引用* 的操作来控制returnSzie
如果使用引用&
更方便改变returnSzie
减少拷贝,提高效率
值传递 是将实参拷贝一份给形参 (形参是实参的拷贝)
引用传递可以避免大量的数据拷贝
返回值
传值返回
int f()
{
int n=0;
n++;
return n;
}
int main()
{
int a=f();
return 0;
}
在传值返回的时候 会产生临时变量
因为在f()函数栈帧销毁后 n也就销毁了
传引用返回
//错误示范
int& f()
{
int n=0;
n++;
return n;
}
int main()
{
int& a=f();
return 0;
}
a和n的地址一样
也就意味着a是n的别名
但是这么做就越界了
在f()结束时 n已经销毁了 但是还能使用a(n的别名)
相当于是 因为引用搞出来的 野指针
属于非法访问
有数据覆盖的风险
那什么时候能用引用访问呢
//正确示范
int& f()
{
static int n=0;
n++;
return n;
}
int main()
{
int& a=f();
return 0;
}
有局部静态变量的时候 就可以使用传引用返回
因为局部静态变量虽然 作用域还是局部的
但是生命周期是全局的 也就是说出了f()函数 并没有销毁
那么它的别名自然也就是合法的 不越界的
补充
int& add(int x,int y)
{
int z=x+y;
return z;
}
int main()
{
int& a=add(2,3);
add(4,5);
cout<<a<<endl;
return 0;
}
输出的是9
因为a是z的别名 当调用完add(2,3)后 z已经销毁
但是调用相同的函数 在同一个地方开辟函数栈帧 所以z的空间 又继续使用了
这时候z是4+5=9 那么作为z的别名 a也就是9
int& add(int x,int y)
{
static int z=x+y;
return z;
}
int main()
{
int& a=add(2,3);
add(4,5);
cout<<a<<endl;
return 0;
}
这样的话 就是2+3=5了
因为静态变量的定义只执行一次
add(4,5)相当于没有产生作用
传值返回和传引用返回的区别
传值返回:会有临时变量
传引用返回:不会进行拷贝,效率更高
总的来说,函数返回时,出了函数的作用域,但返回对象还未销毁(未归还给系统),则使用引用返回和值返回都可以,但是如果已经销毁了,就只能使用值返回。(引用返回有数据覆盖的风险)
引用和指针的对比
相同
引用在语法上是别名 没有独立的空间
指针在语法中是变量 有独立的空间
但在底层视线中 实际上 两者都是相同的
也就是说 在底层实现中 引用也是有空间的
可以通过汇编代码 看简单看一下
不同
- 引用在定义时必须初始化,指针没有强制的语法要求
- 引用在初始化一个实体后,不能再引用其他实体,而指针则是可以改变(在同一类型中)
- 没有NULL引用,但有NUll指针
- sizeof中 引用的的结果是引用类型的大小,而指针一直都是地址空间所占字节的个数(32位平台 占4字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针 但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用则是编译器自己处理
- 引用比指针使用起来更安全一些
内联函数(inline)
回顾宏的知识
int add(int x.inty)
{
return x+y;
}
//实现add的宏
#define add(x,y) ((x)+(y))//可能会忘记加括号 会导致优先级的错误
//场景举例
add(1.2);//((1)+(2))
add(1,2)*5;//((1)+(2))*5 若是外部不加括号 (1)+(2)*5
add(0&1,2|3);//((0&1)+(2|3))----(0+1) 若是内部不加括号 (0&1+2|3) +的优先级高 (0&1&1)
宏的缺陷
1.代码可读性差 容易写错
2.不支持调试(预处理阶段进行了替换)
C++中可替代宏的
C++中内联函数可以代替宏中的函数定义
由于函数调用会开辟函数栈帧 会有一定的时间消耗
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法(内联函数),即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。(利用内联函数 空间换取时间 )
而对应的,在常量方面 const可以代替宏
特点
- inline是以空间换时间的做法,省去开辟函数栈帧的开销,所以如果说代码很长或者循环或者递归不建议使用内联
- inline对编译器来说 只是个建议 不代表只要用inline就会替换代码 如果说代码很长 可能不会替换
- inline函数不能声明和定义分离 会导致链接错误 因为内联函数在编译阶段是不会产生函数的地址的 它利用拷贝代码规避了开辟函数栈帧 所以链接阶段在符号表里找不到inline函数的地址
auto关键字(C++11)
简介
在C++11以前 auto修饰的变量是具有自动存储器的局部变量 但是用的很蛮少的(好像是因为非全局变量默认auto类型 用不用都一样)
在C++11之后 auto史诗级加强了 改变了原来的作用
不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
int add(int x,int y)
{
return x+y;
}
int main()
{
int a=1;
auto b=a;
auto c='c';
auto d=add(a,a);
}
编译器将自动推导出
b是int型,c是char型,d是int型
如果说用auto定义变量时 没有进行初始化
//例如:
auto f;
将无法通过编译 因为auto相当于一个类型声明时的占位符,在编译阶段会将auto替换成变量实际的类型。
使用细则
和指针、引用结合
int x = 5;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
用auto声明指针类型,auto和auto*没有区别,声明引用类型时必须加 &
一行定义多个变量
在同一行定义多个变量时,这些变量必须是相同类型,否则编译器报错。(因为编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a=5,b=10;
auto c=2,d=2.2;//编译错误
不能使用的场景
不能作为形参
以下代码都不可以
int add(auto x,auto y)
{
return x+y;
}
int add(int x,auto y=10)
{
return x+y;
}
因为无法真实地推导参数
并且防止滥用 防止出现
auto add(auto x,auto y)
{
return x+y;
}
如果是这样的话 就相当于没有类型了
不能声明数组
auto a[]={1.2.3.4};
我认为 编译器是通过数组的数据类型来开辟空间储存数据的
而auto声明的变量必须由编译器在编译时期推导而得
这就互相矛盾了
auto的意义
- 在类型名特别长的时候 可以简化代码 让编译器自动推导
- for的范围循环中可以使用
for的范围循环
简介
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int a[] = { 1, 2, 3, 4, 5 };
for(auto& x: a)
x *= 2;
for(auto x : a)
cout << x << " ";
将a数组的每一个数都传给x
如果不使用引用 x只是一个拷贝 并不会改变a的本身
这里的auto 不是必须是auto 也可以是int char double等等
前提是能对应 在这里使用auto是为了方便
使用条件
循环迭代的范围是确定的
void test_for(int a[])
{
for(auto e:a)
{
cout<<e<<endl;
}
}
int main()
{
int a[]={1,2,3,4,5};
teat_for(a);
}
数组传参 实际上传的是指针 无法找到迭代的次数
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
for(beg=v.begin,end=v.end;beg!=end;beg++)//基于传统for循环
{
auto &e=beg;
e *= 2;
}
关键字nullptr
C程序中的NULL
在C语言中,NULL通常被定义为:#define NULL ((void *)0)
NULL实际上是一个空指针,C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
int *pi = NULL;
char *pc = NULL;
C++程序中的NULL
但是以上代码如果使用C++编译器来编译则是会出错的,( 因为C++是强类型语言,void是不能隐式转换成其他类型的指针的)所以实际上编译器提供的头文件做了相应的处理:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
可见,在C++中,NULL实际上是0.( 因为C++中不能把void类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。)
但是实际上,用NULL代替0表示空指针在函数重载时会出现问题
#include <iostream>
using namespace std;
void f(void* i)
{
cout << "f1" << endl;
}
void f(int i)
{
cout << "f2" << endl;
}
int main()
{
f(NULL);
f(nullptr);
return 0;
}
在这段代码中,我们对函数f进行重载,参数分别是void类型和int类型,但是运行结果却与我们使用NULL的目的是不同的,因为本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数,而不是void为形参的函数。
C++中的nullptr
在C++11版本中引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。
总结:
NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,在C++中表示空指针应该使用nullptr更为准确。
关键字nullptr
C程序中的NULL
在C语言中,NULL通常被定义为:#define NULL ((void *)0)
NULL实际上是一个空指针,C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
int *pi = NULL;
char *pc = NULL;
C++程序中的NULL
但是以上代码如果使用C++编译器来编译则是会出错的,( 因为C++是强类型语言,void是不能隐式转换成其他类型的指针的)所以实际上编译器提供的头文件做了相应的处理:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
可见,在C++中,NULL实际上是0.( 因为C++中不能把void类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。)
但是实际上,用NULL代替0表示空指针在函数重载时会出现问题
#include <iostream>
using namespace std;
void f(void* i)
{
cout << "f1" << endl;
}
void f(int i)
{
cout << "f2" << endl;
}
int main()
{
f(NULL);
f(nullptr);
return 0;
}
在这段代码中,我们对函数f进行重载,参数分别是void类型和int类型,但是运行结果却与我们使用NULL的目的是不同的,因为本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数,而不是void为形参的函数。
C++中的nullptr
在C++11版本中引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。
总结
NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,在C++中表示空指针应该使用nullptr更为准确。