C++入门基础
C++的第一个程序
C++兼容C语言,所以在C++中用C语言写也是可以运行的,C++是在C的基础上增加了类和对象、封装、继承、多态等特性。
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
我们接下来就一一来介绍这个程序中的符号含义。
命名空间
namespace的价值
namespace是一个关键字,我们先来看看为什么需要有namespace
#include <stdio.h>
//#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d", rand);
return 0;
}
正常情况下,这个程序是没毛病的,但是如果我加上stdlib.h这个头文件,就会报错
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d", rand);
return 0;
}
这就是C语言的第一个问题----命名冲突,在同一个域中,不能定义同样的,因为会发生冲突,这里rand是C中的一个函数。万一公司里面你和你同事用了一个命名,一合并就玩完了,所以为了解决这个问题,就有了namespace
namespace的定义
• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中
即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下
⾯的rand不在冲突了。
• C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/
类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响
编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期
#include <stdio.h>
#include <stdlib.h>
//域
namespace cbn
{
int rand = 10;
}
int main()
{
printf("%p", rand);//这里的rand访问的就是全局的,注意这里的rand是函数,我们就打印地址就行,否则会报错
printf("%d", cbn::rand);//加上域访问限定符就可以访问域中的了
return 0;
}
再看下面一段程序:
int a = 10;
int main()
{
int a = 1;
printf("%d", a);//这里访问的是局部的a
}
根据就近原则,这里默认访问的是局部的a,因为编译的时候会查找名字的出处,会先在局部找,再在全局找。那我现在就想访问全局的a咋办?
使用域作用限定符::
printf("%d", ::a);
::左边啥都不写,就默认在全局中找
C和C++都有一个原则,使用一个函数都需要找到它的声明和定义,隔离在不同的域,找的时候就不会冲突了。
namespace cbn
{
int rand = 10;
}
比如这里的rand,rand还是定义在全局的,只是会受cbn这个域限制而已,注意,不能把cbn这个域定义在局部。
namespace cbn
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
//如何使用域中的自定义类型:
struct cbn::Node p1;//struct是关键字,我们这里限定的是名字,所以要把访问限定加在结构体的名字前面
return 0;
}
namespace的嵌套定义
• namespace只能定义在全局,当然他还可以嵌套定义。
• 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
//嵌套定义:
namespace cbn
{
namespace cbn1
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
namespace cbn2
{
int rand = 2;
int Add(int left, int right)
{
return (left + right) * 10;
}
}
}
int main()
{
printf("%d\n", cbn::cbn1::rand);
printf("%d\n", cbn::cbn2::rand);
printf("%d\n", cbn::cbn1::Add(1, 2));
printf("%d\n", cbn::cbn2::Add(1, 2));
return 0;
}
在cbn1和cbn2中还可以继续嵌套
多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样
这个我们在后续会经常使用,以后会使用的。
• C++标准库都放在⼀个叫std(standard)的命名空间中。
std就是祖师爷封装的标准库
命名空间的使用
• 指定命名空间访问,项⽬中推荐这种⽅式。
cbn::cbn1::rand
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
namespace cbn
{
int a = 10;
int b = 10;
}
using cbn::a;//把a展开
• 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常⼩练习程序为了⽅便推荐使⽤。
namespace cbn
{
int a = 10;
}
using namespace cbn;
int main()
{
printf("%d", a);//这里就可以直接使用a了
return 0;
}
注意,展开头文件是把头文件内容拷贝过来,编译代码的时候是没有头文件的,因为.h会在用的地方展开,展开命名空间不是拷贝,相当于把命名空间那个域给拆开了。
输入和输出
input output stream,是标准输入、输出流的库,只有特别特别老的编译器才会有iostream.h(类似于vc.60)
像整型、浮点型这些都是内存里面的存储,方便运算,而我们常见的是在文件中,在文件和网络中,就变成字符了
int i = 10;
cout << i;
比如这里,我们cout i,虽然i是int类型的,但是不管是cout 还是printf,这个整型都是转换成了字符再输出出去的。输出到的位置就是我们的控制台(console)。cout和cin输出输入的位置自动是转换到控制台位置,我们如果想输出到文件、数据库也可以,这个后续再介绍
补:控制台在linux平台下叫终端
-
<<是流插⼊运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
-
使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出
int a = 10, b = 9;
cout << " " << a << " " << b;
cin同理,也是自动识别类型
int a, b, c;
double d;
cin >> a >> b >> d;//可以多个
cin >> c;//可以单个
end line的底层不是\n,而是一个函数,它的底层是运算符重载,重载了一个函数指针,但是我们可以简单认为,它的行为就等价于put(‘\n’)
C++会间接包C的文件
建议直接用C的,因为C++的用着不爽。
-
补充:因为C++要兼容C语言,像输入输出它是先会放在缓冲区,到一定程度再刷新出去,所以是需要付出一定代价的,在io需求比较高的地方,如部分大量输入输出的竞赛题或者比赛中,用cout/cin可能oj过不了,效率没有scanf和pritnf高,这时候我们可以考虑C和C++换着用,或者加上这三行代码提高cin/cout的效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
缺省参数
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
//全缺省
void Func1(int a = 10, int b = 10, int c = 29)
{
}
//半缺省
void Func2(int a, int b = 10, int c = 10)
{
}
//这样写不行,必须从右向左连续缺省,不能跳跃,不能从左向右
void Func3(int a = 10, int b, int c = 10)
{
}
void Func4(int a = 10, int b = 10, int c)
{
}
-
我们调用带缺省参数的函数时,C++规定必须从左到右依次给实参,不能跳跃给实参,不能间隔给实参
//全缺省 void Func1(int a = 10, int b = 10, int c = 29) { } //半缺省 void Func2(int a, int b = 10, int c = 10) { } int main() { Func1(); Func1(1); Func1(1,2); Func1(1,2,3); Func1( ,2, );//不能这样传!!! Func2();//不能这样,因为是半缺省,至少传一个 }
缺省参数在实践中的意义
比如我们的栈
void STInit(ST* ps, int n = 4)//n表示开空间大小
{
assert(ps && n > 0);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
这里我们给n个缺省值,就是4,默认开4个空间,假如我们想要1000个空间,那就传第二个参数1000,在初始化就开好空间,避免扩容
int main()
{
ST s1;
STInit(&s1);
// 确定知道要插⼊1000个数据,初始化时直接开好,避免扩容
ST s2;
STInit(&s2, 1000);
return 0;
}
比如:
//Stack.h文件
void STInit(ST* ps, int n = 4);
//Stack.cpp文件
void STInit(ST* ps, int n = 4)//这里重复给n的缺省值就会报错,规定只能在声明中给缺省值
{
assert(ps && n > 0);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
这里主要是考虑到万一你声明和定义给的缺省值不一样,编译器就不知道选哪个了
函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了静态多态行为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的
- 参数类型不同
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
int main()
{
Add(1, 2);
Add(1.1, 1.3);
return 0;
}
- 参数个数不同
void f()
{
}
void f(int a)
{
}
- 参数顺序不同(本质也是类型不同)
void f(int a, char c)
{}
void f(char c, int a)
{}
返回值不同不能作为重载条件,因为编译器无法识别
函数重载的一个大坑
void f()
{
}
void f(int a = 10)
{
}
这俩该调哪一个?这俩不调就没问题,一调就出问题,他们虽然构成函数重载,但是无参调用时会有调用歧义,这解决不了,实在要这样写,把他俩放在不同的域就行。但是这时候他俩就不是函数重载了,因为作用域不同
namespace ccc
{
void f()
{
}
}
void f(int a = 10)
{
}
引用
概念
给已有的空间起别名,就是小名,类似于林冲,外号“豹子头”,取别名不用开空间
类型& 引用别名 = 引用对象
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
引用的特性
- 引用必须在定义的时候初始化
int& ra;//不能这样写,必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,就不能再引用别人,即不能改变引用的指向
int a = 10;
int& b = a;
int& c = a;
int d = 20;
b = d;//这里表示的是赋值,因为引用不能改变指向
C++的引用和Java这类的语言是很不同的,Java的引用可以改变指向,也不是特别需要初始化的,Java的引用在特性上更像C++的指针和引用的结合,C++的引用是为了和指针相辅相成的。
引用的使用
比如以前C语言的Swap函数
void swap(int* px, int *py);
现在用引用就可以
void swap(int& px, int& py);
当然,指针的方法还是可以继续用的。
int main()
{
int a = 10,b = 5;
swap(a,b);//这里px就是a的别名,py就是b的别名
}
现在我们就可以把这个用到栈的实现中,替代一级指针和二级指针,比如
// int STTop(ST& rs)
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top];
}
我们要改变栈,现在传引用过去就行
int main()
{
ST st;
STTop(st) += 10;
return 0;
}
注意,引用没有空引用,也就是不能nullptr&
我们再来上点强度
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
这里的PNode& 是啥?struct ListNode*&
就是struct ListNode* 的别名,以前这里我们需要用二级指针来接收,这里只需要用一级指针的别名即可,也就是这里的PNode
我们以前的传值传参是一个拷贝
void func(int x)//形参x是a的拷贝,它的改变不影响a
{
}
int main()
{
int a = 10;
func(a);//这里就是把a拷贝给形参x
return 0;
}
如果以后a比较大,再使用传值传参,拷贝的消耗就会很大,C语言采用的措施是用指针,我们这里就可以使用引用来解决
void func(int& x)
{
++x;
}
传引用返回会在类和对象再具体说明。
传值返回也会生成一份拷贝。
这个时候,使用传引用返回就会很爽
但是,不是所有情况都要用传引用返回,比如:
int& func()
{
int a = 10;
return a;//这里的a是局部变量,返回别名,就类似于野指针了
}
const引用
只有引用和指针才会存在权限放大和缩小问题0
- 可以引用一个const对象,但是必须用const引用,const引用也可以引用普通对象,因为权限可以缩小,但是不能放大
const int a = 10;
// int& ra = a;//权限放大,只读变成可读可写了,这不合理
const int& ra = a;
int b = 10;
int& rb = b;
const int& cb = b;//权限可以缩小
++b;//不影响原来的权限,是缩小引用的权限
++cb;//这个不能修改
const int& rc = 30;//const引用可以给常量取别名
int c = 2;
int d = 3;
const int& rd = (c + d);//表达式的值存在临时对象中,临时对象具有常性,所以要用const引用
注意看下面的代码:
const int a = 10;
int b = a;//这里不存在权限放大的问题,一定要注意,
int c = a + b;
double d = 12.34;
int i = d;//这里的d会隐式类型转换,中间会产生一个临时变量去存储
const int ri = d;//因为会产生临时变量,临时变量具有常性,所以要用const引用
//这里ri引用的是临时对象,不是d
指针和引用的关系
- 语法上,引用时给变量取别名,不开空间,指针是存储一个变量的地址,要开空间
我们看看汇编就可以,可以看到,指针和引用在底层是一样的,但是,我们平时只谈语法的层面。
- 引用在定义的时候必须初始化,指针不是必须的,但是我们建议初始化
- 引用在初始化引用一个对象后,就不能再引用其他对象;而指针可以改变指向的对象
- 引用可以直接访问指向的对象,指针需要解引用才能访问到
- sizeof中的含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下是4个字节,64位平台下是8个字节)
- 指针很容易出现空指针和野指针的问题,引用很少出现,引用相对来说安全一些
引用也不是绝对安全
int* ptr = NULL;
int& rb = *ptr;//这个可以编译通过
rb++;
但是退出代码不是0,就说明出错了,这里就是空引用,就错了
inline
内联函数
- 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,直接在调用的栈帧里面执行,可以提高效率
- inline对于编译器而言就是一个建议,编译器可以不听,不同的编译器关于inline是否在调用处展开的情况不同,因为C++标准没有规定这个,inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline会被编译器忽略
编译器会觉得你不靠谱,万一你写一个递归很深的函数,那展开不就会代码膨胀吗,编译器长大了,有自己的想法(doge
- C语言实现宏函数也会在预处理时替换展开,但是宏函数实现是很复杂而且容易出错的,且不方便调试,C++设计inline的目的就是为了替换宏
如果要实现两个数相加的宏,你会怎么写?
//宏实现常见问题
//#define ADD(int x,int y) return x + y;
//#define ADD(x, y) x + y;
//#define ADD(x, y) (x + y);
//#define ADD(x, y) (x) + (y)
//正确的宏实现
#define ADD(x,y) ((x) + (y))
//为什么不加分号?-->避免语法错误
//为什么要加外面的括号?-->控制优先级
//为什么要加里面的括号?-->控制优先级
int main()
{
int ret = ADD(1, 2);
cout << ADD(1, 2) << endl;//加了分号就是cout << ADD(1, 2);<< endl;就会报错
cout << ADD(1, 2) * 5 << endl;//->1 + 2 * 5,所以要加外面的括号
int x = 1, y = 2;
ADD(x & y, x | y); // -> (x&y+x|y), &和|的优先级没有+高
return 0;
}
所以,使用宏是非常麻烦的,我们写成一个内联函数就会很方便
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
cout << ret << endl;
return 0;
}
- VS编译器debug版本下默认是不展开inline的,因为方便调试,debug版本想要展开需要设置一下以下两个地方
环境设置为debug x32
- inline不建议声明和定义分离到两个文件,分离会导致链接错误,因为inline被展开,就没有函数地址,链接时会报错
//F.h
#pragma once
#include <iostream>
using namespace std;
inline void f(int a);
//F.cpp
#include "F.h"//这就有内联属性,就不会把f函数的地址放进符号表
void f(int a)
{
cout << a << endl;
}
内联直接放到.h文件就行,不要分离到两个文件
记住,频繁调用的短小函数,适合内联
nullptr
空指针,我们在C语言中使用的是NULL,NULL实际上是一个宏,在传统的C头文件(<stddef.h>)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
但这个NULL有坑
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);//这该调用谁?-->第一个,因为NULL是0
return 0;
}
这时候就会存在歧义,那我可以用强转解决吗?
f((void*)0);//不行,因为void*传这两都不匹配,就需要走隐式类型转换,那隐式类型转换成谁呢?int还是int*?所以还是有歧义
C语言倒是没问题,因为C语言允许void*隐式类型转换成任何类型的指针,但是C++不允许,必须强转
void* a = NULL;
int* b = a;//C语言可以这样写
int* b = (int*) a;//C++必须这样写
所以C++引入了nullptr,它可以隐式类型转换为任何类型的指针,但是,不能转换为整型
int* p1 = nullptr;
int p2 = nullptr;//就这个不行
简单来说,在C++中,我们就不要用NULL了,使用nullptr,可以解决空的问题。
id f(int a)
{
cout << a << endl;
}
内联直接放到.h文件就行,不要分离到两个文件
记住,**频繁调用的短小函数**,适合内联
## nullptr
空指针,我们在C语言中使用的是NULL,NULL实际上是一个宏,在传统的C头文件(<stddef.h>)中,可以看到如下代码:
```c
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
但这个NULL有坑
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);//这该调用谁?-->第一个,因为NULL是0
return 0;
}
这时候就会存在歧义,那我可以用强转解决吗?
f((void*)0);//不行,因为void*传这两都不匹配,就需要走隐式类型转换,那隐式类型转换成谁呢?int还是int*?所以还是有歧义
C语言倒是没问题,因为C语言允许void*隐式类型转换成任何类型的指针,但是C++不允许,必须强转
void* a = NULL;
int* b = a;//C语言可以这样写
int* b = (int*) a;//C++必须这样写
所以C++引入了nullptr,它可以隐式类型转换为任何类型的指针,但是,不能转换为整型
int* p1 = nullptr;
int p2 = nullptr;//就这个不行
简单来说,在C++中,我们就不要用NULL了,使用nullptr,可以解决空的问题。