文章目录
C++ hello world
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
我们会逐句的分析这些代码
命名空间namespace
命名空间的概念
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存 在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
using namespace std;
- 自己写的库,官方库,第三方库,当项目整合之后,我们不可避免遇到命名重复,冲突的问题
- 在C语言中,只有一方相让才能避免这个问题的方式,依此推出命名空间这个语法
#include <stdio.h>
#include <stdlib.h>
int rand = 0;
int main()
{
printf("%d", rand);
return 0;
}
如上,我们rand与stdlib.h中的rand命名冲突了,
#include <stdio.h>
#include <stdlib.h>
namespace lv
{
int rand = 1;
}
int main()
{
printf("%p\n", rand);
printf("%d\n", lv:: rand);
return 0;
}
namespace是关键字,后面的lv是自定义的空间名,可以理解为一个类似的结构体
这个时候我们在主函数调用rand,不指定空间的话,默认是全局库里面的rand,即stdlib文件中的
命名空间定义的是一个域,不创建域去定义rand的话就是全局域中命名冲突了,而namespace就是定义了一个新域,新域的使用方法如下
命名空间的三种使用方法
1.加命名空间名称及作用域限定符
bit::rand
namespace lv { int rand = 1; int Add(int left, int right) { return left + right; } struct node { struct node* next; int val; }; }
- 命名空间可以定义变量 函数 类型
- 命名空间通俗来讲就是在一块空山上,有一个搭建的私人果园,空山就是全局域,谁都可以去,果园就是自己命名的空间,未经许可谁都不能去,::这个操作符就是钥匙许可,有了权限才能进去
注意的是struct Node node的操作符是加在struct后面的 struct bit :: Node node;
2.使用using namespace 命名空间名称引入
如上面,我们一个一个的使用自定义空间的时候都要主动加上域作用操作符,有没有什么办法直接开放权限呢
using namespace bit
这就相当于,把果园的权限全部开放,所有人都可以去采摘
但是这是一个危险的行为,当我们的官方库和bit域中仍然有命名冲突的时候,如rand,编译器就不知道去访问哪个域中的rand了
需要重复写多个一样的代码,并且要指定域情况下,全部展开会危险,我们可以部分展开
3.使用using将命名空间中某个成员引入
授权bit域中的Add,假设全局中也有Add,当编译器编译的时候看见我们对bit域中的Add授权后就会自动去使用bit域中的Add
命名空间可以套娃
namespace lv
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
struct node
{
struct node* next;
int val;
};
namespace xx
{
int rand = 0;
}
}
lv::xx::rand
语句分析
我们写CPP的时候都喜欢全部展开std空间
- std这个域是写在iostream这个头文件里面的,我们展开这个头文件的时候并没有展开std域的权限,默认在全局找
- iostream头文件的展开是拷贝代码过来,其中包括std域的定义
- using namespace std是权限的展开,可以不要这句话,但要有头文件,因为域的定义写在头文件里面
int main()
{
std::cout << "hello" << std::endl;
return 0;
}
using std::cout;
using std::endl;
int main()
{
cout << "hello" << endl;
return 0;
}
注意,一个或多个文件中的同一个命名空间会自动合并
输入输出函数
- C语言中的输入输出函数是scanf和printf
- C++的就是cin和cout,in就是输入,out是输出,前面的c是console(控制台)的意思
- cin就是控制台输入,cout就是控制台输出
- 这2个函数是包含在iostream头文件里面的,涉及了类和对象,函数重载的知识
cout << "hello world"<< endl;
<<是流插入运算符,hello world流向cout这个对象
endl 是end of line 换行的意思
int i = 0;
cin >> i;
>>是流提取运算符,在控制台提取一个值给到i变量里
C++的输入输出可以自动识别类型,C语言要加类型符号,这里面用到的是函数重载
缺省函数
缺省的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参
通俗来讲就是备胎的意思
缺省函数的分类
- 全缺省参数
这种不行,只能是从左往右显示传参
- 半缺省参数
使用场景
typedef struct stack
{
int* a;
int top;
int capacity;
}stack;
void initst(stack* p)
{
p->a = NULL;
p->top = 0;
p->capacity = 0;
}
void pushst(stack* p)
{
//.....
}
ok我们很多代码都是这样写的,当我们在主函数里面明确要插入100个数据时候,我们每轮扩容4个,太low了,并且扩容有内存的消耗,很多书上都这样写
#define N 4;
void initst(stack* P)
{
p->a = (int*)malloc(sizeof(int) * N);
p->top = 0;
p->capacity = 0;
}
这样写也不能避免扩容问题,而且也不能随着外部需求改变
使用缺省函数即可
void initial(stack* p, int N = 4)
{
p->a = (int*)malloc(sizeof(int) * N);
p->top = 0;
p->capacity = 0;
}
注意,倘若声明和定义分开的话,声明和定义不能同时给缺省参数,声明给定义不给
重载
重载的概念
**函数重载:**是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似 数据类型不同的问题。
-
简单来说在C++里面重载就是一词多义的意思
-
C语言是不允许函数同名的情况
-
C++可以,但要求构成重载函数(函数名相同,参数不同)
-
这里的参数不同,有3种
1.参数个数不同
void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; }
2.参数类型不同
int add(int a, int b) { cout << "add(int a, int b)" << endl; return a + b; } int add(double a, double b) { cout << "add(double a, double b)" << endl; return a + b; } //下面这个也算参数类型不同 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; }
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; } 这里的顺序不是形参名的顺序,是类型的顺序
注意,单纯的返回值不同不能构成重载,在学了重载是如何实现的就知道了
重载函数是如何实现的
我们的重载函数,编译器是如何对应找到我们的函数的呢?要从C++的编译链接说起
以我们的汇编代码来看,函数的跳转是call 函数地址,在编译链接过程中生成符号表
在C语言中,函数名充当符号表,有2个一样的重名函数,编译器区分不开
倘若C++继续使用这个函数名充当符号表,那么C++就不支持重载函数了,故C++有一个函数名修饰规则
如图,将2个重名的函数,修饰成了不同的新名,这样编译器就可以对应找的函数
注意的是,函数的地址是第一句指令的地址,如果只有声明没有定义,编译器就会报错,找不到函数的地址
然而我们的C++的函数名修饰规则不是那么的直观,在Linux下就直观一些了
_Z4funcid:
- _Z是前缀
- 4是函数名的字节大小
- func是函数名
- id是形参的首字母,即int double
我们函数的类型不同,修饰出来的名就不同,所以编译器可以清楚的知道我们要调用哪一个函数,这就是重载函数
这也说明了,单纯的函数返回值不同不能构成重载,因为函数名修饰规则里面没有带返回值,我们主要要看参数的类型
引用
引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
类型& 引用变量名(对象名) = 引用实体
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
- 注意:引用类型必须和引用实体是同种类型的
- 如上图代码,ra就是a的别名,也就是说a所在的空间有ra和a 2个名字,空间地址一样
- 对ra操作就是对a操作,反之也是
- 不能与取地址符号混淆
引用的使用
做参数
引用在特殊场景下有妙用,如在swap函数中
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1;
int b = 2;
swap(a, b);
cout << a << endl;
cout << b << endl;
return 0;
}
我们可以在形参上取别名,即形参就是实参的别名,这样改变形参就可以改变到实参了
再比如说我们的单链表
typedef struct list
{
int num;
struct list* next;
}list;
//C语言写法
void pushback(list** node, int x)
{
list* newnode;
if (!*node)
{
*node = newnode;
}
else
{
//...
}
}
//C++写法
void pushback(list*& node, int x)
{
list* newnode;
if (node)
{
node = newnode;
}
else
{
//...
}
}
做返回值
我们先看下面代码
注意count的n不是直接返回给主函数的,n出栈帧就销毁了,而是存在一个临时变量里
我们的返回值引用
可能会问,为什么引用可以拷贝给ret,是因为引用的本质是取别名,相当于同一块空间有多个名字,这和n直接拷贝给ret等价
n的空间销毁了,但是ret去访问这块空间,这是危险的
有2种可能,第一种就是1,第二种是随机值
相当于你住酒店,退房前留下一个苹果,退房后再去访问这个房间,苹果还在不在是不确定的,在不同编译器下,有的空间释放就会置为随机值,有的不会,要看具体的环境,如上面的栈帧销毁后,就不会被置成随机值
如果我们再把ret作为n的引用,调用2次cout打印ret为什么出现一次1一次随机值?
因为cout的本质也是函数,也要建立栈帧,在什么地方建立栈帧呢?在之前count函数的那个空间上(销毁出栈,cout函数压栈),只是开大开小的问题,这样我们的cout函数就会覆盖掉之前n所在的空间,故此是随机值了
但为什么,第一次不是随机值?因为第一次cout调用,ret是作为的参数,那个时候n还没有覆盖掉,第二次才覆盖
那这样呢
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
增强效率:以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直 接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效 率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
再比如说我们的链表,我们需要读取,并且修改一个位置的值时候
typedef struct list
{
int arr[10];
int sz;
}list;
//读取
int get(list* p, int i)
{
return p->arr[i];
}
//修改
void change(list* p, int i, int x)
{
p->arr[i] = x;
}
如图,C语言需要2个接口
int& at(list& p, int i)
{
return p.arr[i];
}
int main()
{
list s;
at(s, 0) = 0;
at(s, 1) = 1;
at(s, 2) = 2;
return 0;
}
C++利用引用只需要一个接口,当然在C语言中用指针也可以完成,但是没有C++这样舒服
#define N 10
struct node
{
//成员函数
int& at(int i)
{
return a[i];
}
//成员变量
int a[N];
};
在C++里面struct升级成类了,里面可以定义函数等
int main()
{
struct node s; //兼容C的定义
node s; //C++可以直接这样定义
retrurn 0;
}
int main()
{
node s;
int i = 0;
for (i = 0; i < N; i++)
{
s.at(i) = i;
}
for (i = 0; i < N; i++)
{
cout << s.at(i) << " ";
}
cout << endl;
return 0;
}
引用的条件
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
常引用
如图这样写,编译器不能通过
如图,a是const修饰的,即在语法上不能被修改,而起别名b之后,并没有给b也加上限制,这a可以通过b进行修改,这是权限的放大,是不允许的
- 很多时候都会产生临时变量
- 临时变量是常量
- 1中临时变量可以拷贝给ret,不涉及什么权限放大
- 2中就是权限放大了,一个常量交给一个变量是不合语法的,加上const修饰即可
引用和指针的区别
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
我们来看引用和指针的底层
在底层的角度,引用就是指针