目录
一 . C++的第一个程序
二 . 命名空间
2.1 namespace的价值
2.1 namespace 的定义
7.3 命名空间的使用
三 . C++输入&输出
四 . 缺省参数
五 . 函数重载
六 . 引用
6.1 引用的概念和定义
6.2 引用的特性
6.3 引用的使用
6.4 const 引用
6.5 指针和引用的关系(笔试常考)
一 . C++的第一个程序
C++兼容C语言绝大多数的语法,所以C语言实现的 Hello World! 依旧可以运行 , C++中需要把定义文件代码后缀改为 .cpp , vs 编译器看到的是 .cpp 就会调用C++编译器编译,linux下要用g++编译,不再是gcc
#include <stdio.h>
int main()
{printf("Hello World!");return 0;
}
当然 , c++也有自己的一套输入输出 , 代码如下 :
#include<iostream>
using namespace std;
int main()
{cout << "Hello World!"<<endl;return 0;
}
二 . 命名空间
2.1 namespace的价值
在c++中,变量 、函数 和后续需要学到的类都是大量存在的 , 这些变量 、函数和类的名称都存在于全局作用域中 , 可能会导致很多冲突 。使用命名空间的目的是对标识符的名称进行本地化 , 以避免命名冲突和名字污染 , namespace 关键字的出现就是针对这种问题的 。
c语言项目类似下面程序这样,出现了命名冲突的问题 , c++ 引入namespace 就是为了更好的解决这类问题 !
全局变量 rand 与 头文件 stdlib.h中 的函数rand 名字发生冲突 , namespace的引入是为了更好的解决这一个问题 。
2.1 namespace 的定义
-
定义命名空间 , 需要使用到namespace 关键字 , 后面跟命名空间的名字 , 然后使用花括号 { } 即可 , { } 里即为命名空间成员。命名空间中可以定义变量/函数/类型等 。
-
namespace 本质是定义出一个域 , 这个域跟全局域各自独立 , 不同的域可以定义同名变量 ,所以 上述代码 的rand 不再冲突了 。
#include <stdio.h>
#include <stdlib.h>
namespace bit
{int rand =10 ;
}
int main()
{printf("%d\n", bit::rand);return 0;
}
这里的 :: 符号 : 命名空间限定符 , 用于指定特定的作用域或者是命名空间 ;例如 bit :: rand 表明 我们需要访问的是bit 命名空间下的rand类型 , 如果没有这个符号,可能导致命名冲突 。
-
C++中域有函数局部域 , 全局域 , 命名空间域 , 类域 ; 域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑 , 所以有了域隔离 , 名字冲突就解决了 。局部域和全局域除了会影响编译查找逻辑 , 还会影响变量的生命周期 , 命名空间域和类域不影响变量的生命周期 。
#include <stdio.h>
#include <stdlib.h>
namespace bit
{int rand =10 ;
}
int main()
{//这里指的是访问全局的rand函数的地址printf("%p\n", rand);//这里是访问指定命名空间bit里的randprintf("%d\n", bit::rand);return 0;
}
-
namespace 只能定义在全局 , 当然也可以嵌套定义。
#include <stdio.h>
namespace bit
{namespace zhangsan{int age = 18;int add(int x, int y){return x + y;}}namespace lisi{int age = 19;int add(int x, int y){return x + y;}}
}
int main()
{printf("%d\n", bit::zhangsan::age);printf("%d\n", bit::lisi::age);printf("%d\n", bit::zhangsan::add(1, 2));printf("%d\n", bit::lisi::add(3, 2));return 0;
}
-
项目工程中多文件中定义的同名 namespace 会认为是一个namespace , 不会冲突。
stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>namespace bit
{typedef int STDataType;typedef struct Stack{STDataType * a;int top;int capacity;}ST;void STInit(ST * ps, int n);void STDestroy(ST * ps);void STPush(ST * ps, STDataType x);void STPop(ST * ps);STDataType STTop(ST * ps);int STSize(ST * ps);bool STEmpty(ST * ps);
}
stack.c
#include"Stack.h"
namespace bit
{void STInit(ST * ps, int n){assert(ps);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;}// 栈顶void STPush(ST * ps, STDataType x){assert(ps);// 满了, 扩容if (ps->top == ps->capacity){printf("扩容\n");int newcapacity = ps->capacity == 0 ? 4 : ps->capacity* 2;STDataType * tmp = (STDataType*)realloc(ps->a,newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}ps->a = tmp;ps->capacity = newcapacity;}ps->a[ps->top] = x;ps->top++;}}
-
c++ 标准库都放在一个叫std (standard) 的命名空间中 。
7.3 命名空间的使用
编译查找一个变量的声明/定义时 , 默认只会在局部或者全局查找 , 不会到命名空间里面去查找 , 所以下面程序会编译报错 。 所以我们要使用命名空间中定义的变量/函数,有三种方式 :
1 . 指定命名空间访问 , 在项目中推荐这种方式。
2 . using 将命名空间某个成员展开 , 项目中经常访问的不存在冲突的成员推荐这种方式。
3 . 展开命名空间中全部成员 , 项目不推荐 , 冲突风险很大 , 日常小练习程序为了方便推荐使用 。
总之就是 , 想使用命名空间的成员就需要告诉编译器,这个变量要到命名空间里找
-----> 1 . 可以用 :: (命名空间限定符)
-----> 2. using 展开需要使用的命名空间
#include <stdio.h>
namespace bit
{int a = 10;int b = 20;
}//编译报错:"a"未声明标识符
int main()
{printf("%d\n", a);return 0;
}//指定命名空间访问
int main()
{printf("%d\n", bit::a);return 0;
}//using将命名空间中某个成员展开
using bit::a;
int main()
{printf("%d\n", a);printf("%d\n", bit::b);return 0;
}//展开命名空间中全部成员
using namespace bit;
int main()
{printf("%d\n", a);printf("%d\n", b);return 0;
}
三 . C++输入&输出
- <iostream> 是 Input Output Stream 的缩写 , 是标准的输入,输出流库 , 定义了标准的输入,输出对象。
- std::cin 是istream 类的对象 , 它主要面向窄字符 ( narrow characters (of type char)) 的标准输入流 。
- std::out 是 ostream 类的对象 , 它主要面向窄字符的标准输出流。
- std::endl 是一个函数 , 流插入输出时 , 相当于插入一个换行字符 + 刷新缓冲区。
- << 是流插入运算符 , >> 是流提取运算符 。(c语言还用这两个运算符做位运算左移/右移)
- 使用c++ 输入输出更方便 , 不需要像print / scanf 输入输出时那样 , 需要手动指定格式 , c++ 的输入输出可以自动识别变量类型 ( 本质是通过函数重载实现) , 重要的是 , c++流能更好的支持自定义类型对象的输入输出。
- IO流涉及类和对象 , 运算符重载 , 继承等很多面向对对象的知识
- cout / cin / endl 等都属于c++ 标准库 , c++ 标准库都放在一个叫 std ( standard ) 命名空间中 , 所以需要通过命名空间的使用去使用他们 ---------------> using namespace std;
日常练习中 , 我们可以使用 using namespace std , 但是在实际项目开发中 , 不建议使用。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;int main()
{int a = 0;double b = 0.1;char c = 'a';cout << a << " " << b << " " << c << endl;std::cout << a << " " << b << " " << c <<std:: endl;//没有包含<stdio.h>,也可以使用print和scanf,在包含<iostream>间接包含了//vs编译器是这样的,其他编译器可能会报错scanf("%d%lf", &a, &b);printf("%d %lf\n", a, b);//可以自动识别类型cin >> a;cin >> b >> c;cout << a << endl;cout << b << " " << c << endl;return 0;
}
在io需求比较高的地方,如部分⼤大量输入的竞赛题中,加上以下3行代码,可以提高C++IO效率
#include<iostream>
using namespace std;
int main()
{
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
四 . 缺省参数
1 . 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
2 . 全缺省就是全部形参给缺省值 , 半缺省就是部分形参给缺省值 。 C++ 规定半缺省参数必须从右往左依次连续缺省 , 不能间隔跳跃给缺省值 。
3 . 带缺省参数的函数调用 , C++规定必须从左到右依次给实参 , 不能跳跃给实参 。
4 . 函数声明和定义分离时 , 缺省参数不能在函数声明和定义中同时出现 , 规定必须函数声明给缺省值 。
#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;
}
五 . 函数重载
C++支持在同一作用域出现同名函数 , 但是要求这些同名函数的形参不同 , 可以是参数个数不同或者类型不同 。 这样C++函数调用就表现出了多态行为,使用更灵活 。 C语言是不支持同一作用域中出现同名函数的 。
----> 同名函数,要求形参不同 (参数不同 或者 类型不同 )
#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;}void f1(){cout << "f()" << endl;}void f1(int a = 10){cout << "f(int a)" << endl;}int main()
{Add(10, 20);Add(10.1, 20.2);f();f(10);f(10, 'a');f('a', 10);return 0;
}
返回值不同不能作为重载条件 , 因为调用时也无法区分 , 会报错 , 存在歧义 , 编译不知道调用谁 !
void fxx()
{
}
int fxx()
{return 0;
}
六 . 引用
6.1 引用的概念和定义
引用不是新定义一个变量 , 而是给已存在变量取了一个别名 , 编译器不会为引用变量开辟内存空间 , 它和它引用的变量共用同一块内存空间 。
格式 :
类型& 引用别名 = 引用对象 ;
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;int main()
{int a = 0;//引用 : b 和 c 是 a 的别名int& b = a;int& c = a;//也可以给别名取别名int& d = b;++d;//指向同一块的空间cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
这里引用可以拿水浒传中的李逵作比 , 宋江叫他“铁牛” , 江湖人称 " 黑旋风“ ;铁牛,李逵,黑旋风都是同一个人 ;
C++ 中为了避免引入太多的运算符 , 会复用C 的一些符号 ,比如前面的 << 和 >> , 这里引用 也和 取地址 使用了同一个符号 & , 大家注意区分
6.2 引用的特性
- 引用在定义时 , 必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体 , 就不能再引用其他实体
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;int main()
{int a = 0;int& b = a;int& c = a;int& d = b;int e = 10;b = e;cout << a << endl;cout << b << endl;cout << c << endl;cout << d << endl;cout << e << endl;cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;cout << &e << endl << endl;return 0;
}
6.3 引用的使用
1 . 引用在实践中主要是用于引用传参 和 引用返回值中减少拷贝提高效率 和 改变引用对象时同时改变被引用对象 。
2 . 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
3 . 引用返回值的场景相对比较复杂 , 这里简单介绍以下场景 , 在后续的类和对象的博客中,还会继续深入介绍 。
4 . 引用 和 指针 在实践中相辅相成,功能有重叠性 , 但是各有特点 , 互相不可替代 。 C++的引用跟其他语言的引用 ( 如Java ) 是有很大的区别的 , 除了用法 , 最大的点 , C++引用定义后不能改变指向 , Java 引用可以改变指向 。
1 . 下面举一个我们熟悉的交换函数Swap , 来体会以下 引用 的 ”巧“ :
指针传参版本 :
#include<iostream>
using namespace std;void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}
int main()
{int x = 1;int y = 2;cout << "交换之前:x = " << x << " y = " << y << endl;Swap(&x, &y);cout << "交换之后:x = " << x << " y = " << y << endl;return 0;
}
引用传参版本:
void Swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}
int main()
{int x = 1;int y = 2;cout << "交换之前:x = " << x << " y = " << y << endl;Swap(x, y);cout << "交换之后:x = " << x << " y = " << y << endl;return 0;
}
2 . 紧接着再来看看之前实现过的数据结构 -- 栈 , 使用引用传参 :
1 ) 引用后 , 形参与实参指向的空间是同一个 , 传参的时候不需要传递地址2 ) 访问结构体的时候 不使用 -> 操作符 , 而使用 . 操作符
如果想使用引用传参来实现数据结构栈 , 可以看之前博客 : 栈与队列的常见接口的实现-CSDN博客 , 试着把指针传参改为引用传参
#include <assert.h>
#include<iostream>
using namespace std;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)
{// 满了, 扩容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)
int& STTop(ST & rs)
{assert(rs.top > 0);return rs.a[rs.top];
}int main()
{
// 调⽤全局的ST st1;STInit(st1);STPush(st1, 1);STPush(st1, 2);cout << STTop(st1) << endl;STTop(st1) += 10;cout << STTop(st1) << endl;return 0;
}
3 . 一些主要用C代码实现版本数据结构的教材中 , 会使用C++引用传参 , 来代替指针传参 , 目的是简化程序 , 避开复杂指针 ---> 一级指针 , 二级指针 ...;
//使用引用来代替一级指针
typedef struct SeqList
{int a[10];int size;}SLT;// ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针
void SeqPushBack(SLT& sl, int x)
{}//void SeqPushBack(SLT* sl, int x);
//void SeqPushBack(SLT& sl, int x)
//使用引用来替代二级指针
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)#include<iostream>
using namespace std;typedef struct ListNode
{int val;struct ListNode* next;
}LTNode, *PNode;// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序void ListPushBack(PNode& phead, int x)
{PNode newnode = (PNode)malloc(sizeof(LTNode));newnode->val = x;newnode->next = NULL;if (phead == NULL){phead = newnode;}else{//...}
}int main()
{PNode plist = NULL;ListPushBack(plist, 1);return 0;
}
注意 : 这里的Pnode 是结构体指针变量 。对于二级指针引用 ,拿 int 型做比, 只能是
----> int* & 引用名 = 引用对象 ;( 为整型指针变量给别名)
不会有 int & * 引用名 = 引用对象 ,这样的定义,不符合引用的格式
4 . 引用返回值
下面的STTop(S) += 10 , 会报错 ,因为函数返回的时候 , 返回的是临时对象 , 具有常性 , 不可以更改 !
如果想要实现 STTop(S) += 10 , 使用引用 , 返回对象的别名
为什么需要有临时对象的概念?直接返回不行吗?
----> 下面的代码 , top 如果出了作用域 , 生命周期结束了,不使用 临时对象/寄存器存储 , top 就找不到了
6.4 const 引用
1 . 可以引用一个const 对象 , 但是必须用 const 引用 。 const 引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小 , 但是不能放大 。
2 . 需要注意的是 类似 :
int & rb = a*3 ; double d = 12.34 ; int& rd = d ;这样一些场景下 a* 3的结果保存在一个临时对象中 , int& rd = d 也是类似 , 在类型转换中会产生临时对象存储中间值 , 也就是 , rb 和 rd 引用的都是临时对象 , 而 C++ 规定临时对象具有常性 , 所以这里就触发了权限放大 , 必须要用常引用才可以 。
3 . 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时 -----> 临时创建的一个未命名的对象 , C++中把这个未命名的对象叫做临时对象 。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;int main()
{const int a = 10;//权限不可以放大//int& ra = a;//权限可以平移const int& ra = a;//权限可以缩小int b = 20;const int& rb = b;return 0;
}
//引用传参,减少拷贝,建议用引用传参,好处是实参可以是常量/运算表达式等
void Func(const int& r)
{}
int main()
{int a = 10;const int& ra = a;const int& rra = 30;const int& rb = a * 3;double d = 13.14;const int& rd = d;Func(a);Func(10);Func(a * 3);Func(d);return 0;
}
6.5 指针和引用的关系(笔试常考)
C++中指针和引用就像两个性格迥异的亲兄弟 , 指针是哥哥 , 引用是弟弟 , 在实践中 , 他们相辅相成 , 功能具有重叠性 , 但是各有自己的特点 ,互相不可以替代 。
- 语法概念上 : 引用是一个变量的取别名 ---> 不开空间 , 指针是存储一个变量的地址 , 需要开辟空间 。
- 引用在定义时必须初始化,指针建议初始化,但是在语法上不是必须的。
- 引用初始化时引用一个对象 , 就不能再引用其他对象 ; 而指针可以在不断地改变指向对象 。
- 引用可以直接访问指向对象 , 指针需要解引用才能访问指向对象。
- sizeof 中含义不同 , 引用结果为引用类型的大小 , 但指针始终是地址空间所占字节个数(32位平台下 --> 4个字节 , 64 位平台下 8 字节)
- 指针很容易出现空指针和野指针的问题 , 引用很少出现 , 引用使用起来相对更安全一些 。