【C++初阶】C++入门
1、C++第一个程序
C++是脱胎于C语言的,所以也包含了C语言绝大多数的内容,C++兼容C语言绝大多数的语法,在C语言中能实现的程序在C++中也是可以执行的,但需要将定义文件代码的后缀改为.cpp
就比如hello world程序
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
但在C++中,它是自成体系的,有自己的输入输出 c++版的hello world程序为
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world\n"<<endl;
return 0;
}
初学C++的小伙伴出看见这个代码会有很多疑惑,但不要慌张,接下来让我来为你详细解释
2、命名空间:namespace
在看很多书上的C++代码都很看到这样的一行代码“using namespace std;”,
都会疑惑namespace是什么?在这里有什么作用?
不要急,我们慢慢来
我们要知道在C语言中我们经常会因为变量和函数的命名冲突而感到困惑,而我们的C++的祖师爷可能也是深受它的毒害吧,就创造了一个新概念,叫做命名空间(namespace)
使用命名空间的目的就是对标识符的名称进⾏本地化,以避免命名 冲突或名字污染,namespace关键字的出现就是针对这种问题的
namespace的定义
1、
定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中
即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
namespae 空间名
{
}
命名空间的本质是域,在C语言中我们学过了全局域和局部域,而我们在C++中学习的命名空间这个域独立于全局域,又因为不同的域可以定义同名的变量,在这里我们也可以知道,命名空间域中的变量也和全局变量一样,只有在程序结束后才会结束生命周期
总结就是:命名空间域内的变量和全局变量类似
命名空间会影响编译查找变量的规则,使得C++中查找规则是:先查找局部域,在查找全局域,一般不会在命名空间中查找
2、
namespace只能定义在全局域中,而且它还可以嵌套定义:就是在一个namespace中再定义一个namespace(可以无限套娃,但最多嵌套两三次就足够了)
这里的嵌套定义就类似于一个公司里面有很多部门,每个部门就相当于一个namespace,而每个部门里面又有很多员工,每个员工又是一个独立的namespace
嵌套定义后使用命名空间就需要由外向里的进行寻找
3、
多个文件中定义了多个同名的namespace的时候,并不会发生冲突,它会自动合并为一个命名空间
4、
C++的标准库都放在std的命名空间中,这就是为什么许多C++代码都会有“using namespace std;
namespace的使用
1、指定命名空间访问(项目中推荐使用)
2、using将命名空间中的某个成员展开,这适用于项目中经常使用而且不存在冲突的成员
3、using将命名空间中全部的成员展开,这种方法在项目中不推荐使用,而且当出现多个using的展开可能会出现冲突
using n::b;
int main()
{
printf("%d ",n::a);//指定命名空间访问
printf("%d ",b);//using将命名空间中的某个成员展开
return 0;
}
using namespace n;//展开命名空间中的全部成员
int main()
{
printf("%d",a);
return 0;
}
3、C++的输入输出
<iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输入流。
std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。
<<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊
输出可以⾃动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是
C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
IO流涉及类和对象,运算符重载、继承等很多⾯向对象的知识,这些知识我们还没有讲解,所以这
⾥我们只能简单认识⼀下C++ IO流的⽤法,后⾯我们会有专⻔的⼀个章节来细节IO流库。
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要 通过命名空间的使⽤⽅式去⽤使用它们
4、缺省参数(默认参数)
缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值,也就是给函数的形参一个默认值。在调用该函数时,如果没有给明实参就采用该形参的默认值(缺省值)否则就使用给明的实参
缺省参数又分为全缺省和半缺省参数
全缺省:函数的全部形参都给缺省值
半缺省:就是部分形参给缺省值
C++规定半缺省参数必须从右往左依次连续缺省,不能中间跳跃给实参;而且带缺省参数的函数调用时,C++规定必须从左往右依次给实参,不能跳跃给实参
缺省参数不能在函数声明和定义中同时出现,且规定必须函数声明给缺省值
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使⽤参数的默认值
Func(10); // 传参时,使⽤指定的实参
return 0;
}
#include <iostream>
using namespace std;
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
// 半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func1();
Func1(1);
Func1(1,2);
Func1(1,2,3);
Func2(100);
Func2(100, 200);
Func2(100, 200, 300);
return 0;
}
5、函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
函数重载主要是根据形参来进行区别
1、形参类型不同
2、形参个数不同
3、形参顺序不同
#include<iostream>
using namespace std;
// 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;
}
这里要注意的是返回值不同的同名函数不构成重载,编译器无法进行区分
int f(int a)
{
cout << "f(int a)" << endl;
return 0;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f(10);
f();
return 0;
}
在多个同名函数中,其中有函数中含有缺省参数时我,虽然在传参时构成重载,但是在不传参时会发生编译错误,因为编译器不知道应该调用哪个函数(这里就涉及到了二义性)
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(10);
return 0;
}
6、引用
C++中引入了一个新的概念叫做:引用,引用顾名思义就是用别人的,而不是自己的,所以,引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间(在这里是是指代语法层)。
类型& 引用别名 = 引用对象
引用的特性
1、引用在定义时必须初始化;
2、一个变量可以有多个引用;
3、引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
int main()
{
int a = 0;
// 引用:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 编译报错:“ra”: 必须初始化引用
//int& ra;
int* c = &a;
int*& rc = c;//指针也可以取别名
// 这里取地址我们看到是一样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
int x = 10;
d = x;//在这里是赋值拷贝
return 0;
}
引用的使用
引用的实践主要是在引用传参和引用做返回值两个方面
引用传参:
引用传参跟指针传参功能是类似的,它其实就是代替了指针的传址,而且也不需要像指针一样要解引用和取地址,并且引用不需要开辟空间,减少了拷贝,极大地提高了效率
//引用传参
void Swap(int& ra, int& rb)
{
int temp = ra;
ra = rb;
rb = temp;
}
int main()
{
int a = 1, b = 2;
printf("a=%d,b=%d\n", a, b);
Swap(a, b);
printf("a=%d,b=%d\n", a, b);
return 0;
}
引用做返回值:
引用做返回值的本质是:不会生成临时变量,直接返回对象别名(这里的别名指的是给被引用对象所处空间起的别名)
在引用做函数返回值时,函数结束后函数栈帧会被销毁,却不会影响实参及其别名所处的空间,所以用引用做返回就能起到指针传址调用一样的作用,改变实参
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST& rs, int n = 4)//在这里也是用到引用传参
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{
//assert(ps);
// 满了, 扩容
if (rs.top == rs.capacity)
{
printf("扩容\n");
int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top++;
}
/*引用做返回值*/
int& STTop(ST& rs)
{
//assert(rs.top > 0);
return rs.a[rs.top - 1];
}
int main()
{
ST s;
STInit(s);
STPush(s, 1);
STPush(s, 2);
STPush(s, 3);
STPush(s, 4);
//传值
/*int top = STTop(s);
top+=10;
cout << top << endl;*/
//引用传参
/*STTop(s) += 10;
cout << STTop(s) << endl;*/
return 0;
}
在这里是使用传值返回,在C/C++中规定,返回类型除引用以外,返回对象是先存储在一个临时变量(或者寄存器)当中,再由临时变量传到主函数中,临时变量的建立需要开辟空间,再由主函数中建立一块空间接收,所以主函数中就需要在建立一个同类型变量接收临时变量的值
/*传值*/
int STTop(ST& rs)
{
return rs.a[rs.top - 1];
}
错误示范,不能返回局部对象的引用
在该函数结束后,函数栈帧销毁,top也会被销毁,这时的别名就会像free(指针)后,没有给指针置空一样,成为野别名
虽然编译器不会报错,但是也无法实现功能
int& STTop(ST& rs)
{
int top = rs.a[rs.top - 1];
return top;
}
const引用
可以引用一个const对象,但必须const引用,这里就涉及了权限的平移(const->const)
const int a=1;
const int& ra=a;//权限的平移
const引用也可以引用普通对象,这里就涉及了权限的缩小(由变量变为常量)
int a=10;
const int& ra=a;
//权限的缩小
但权限不能放大
const int& a=1;
int& ra=a;//错误
在对象权限的访问中,权限可以平移、缩小,但不能放大 ,这里的对象指的是指针和引用
临时变量(临时对象 )
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,
C++中把这个未命名对象叫做临时对象
在被引用对象为常量、表达式、类型不同(会涉及到类型转换)时要用到临时变量,而C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤(const)才可以。
int main()
{
//临时变量的常性(具有const的属性)
//常量
//int& x = 10;//错误的,要引用一个常量,就要在前面加一个const
const int& x = 10;
cout << x << endl;
//表达式
int p = 2;
//错误
//int& rp = p*2;//在这里就涉及到了临时变量
//p*2的和存储在一个临时变量里面,而临时变量具有const属性,
//所以接收值也应该具有const属性,就需要在前面加一个const(都是涉及权限的放大)
const int& rp = p*3;
cout << rp << endl;
//类型不同
double d = 1.1;
//int& rd = d;//在这里值也是存储在一个临时变量当中,所以也要用const来接收(都是涉及权限的放大)
const int& rd = d;
cout << d << endl;
return 0;
}
当我们在函数传参,用到引用的时候,传入的值就如上面的三种情况时就需要用const引用了
在现在学习到的场景下,类型转换和传值返回会用到临时对象
指针与引用的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
1、
语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。但在汇编层面上,其实引用还是以指针的形式运行的
2、
引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
3、
引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
4、
引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
5、
sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
6、
指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
7、nullptr
在C语言中NULL其实是一个宏,头文件为(stddef.h)
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由代码可得C++中NULL可能被定义为0或者被定义为无类型(void*)的常量
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
//f(NULL); // 调用这个函数时原本要为f(int* ptr),但因为被定义为0,所以结果为f(int x),错误
//f((void*)0); // 该函数调用会报错
f(nullptr);
return 0;
}
所以在C++11中引入了nullptr,它是一个特殊关键字,他可以转换成任意类型的指针类型,使用它可以避免类型转换的问题,因为它可以隐式转换为指针类型,而不会转换为整数类型