C++基础
头文件的引用
☘️例:
#include <string>
#include <iostream>
在 C++ 不会像 C 一样引用头文件后缀名 “.h”,例如 stdio.h;因为在不同平台上面 C++ 的头文件的后缀名不一定相同,并且 iostream 和 iostream.h 是两个不同的文件
☘️当我们要使用C中的头文件时,可以如下:
#include <cstdio> //在文件名前加个 c,然后删掉后缀名
#include <cassert>
命名空间 namespace
☘️作用:
- 一个大型项目中会需要多人协作完成,其中难免发生变量、函数、类重名的现象,为了避免这种情况发生,就有了命名空间
- 我们使用的变量如果与库里的变量同名也可以使用定义命名空间的方法解决
☘️命名空间的创建
namespace name1 {
int cout = 3;
string cin = "name1::cin"; //此时不是 std::cin
int fun(int a, int b);
}
namespace name1{
int cerr = 5;
int* a1 = new int(10);
class tmp;
} //命名空间可以合并,在编译时上面两个相同的命名空间会自动合并
//命名空间还可以嵌套创建
namespace name2 {
int cout = 3;
string cin = "name2::cin";
namespace name3{
int cout = 4;
string cin = "name2::name1::cin";
}
}
☘️命名空间的使用
关键字 using—— 将命名空间里的所有东西都展开到全局
using namespace name1; //将 name1 中的所有成员扩展到全局
using name2::count; //只将 name2 中的成员 count 扩展到全局
using namespace std; //将std里的标识符(变量、函数)扩展到全局
//std 是包含 C++ 标准库的命名空间,里面包括 cout、cin……
using name2::name3::cout; //嵌套创建的命名空间必须嵌套使用
☘️总结
- using namespace xx
将 xx 命名空间里的所有东西展开到全局,如 using namespace std
优点:方便
缺点:把自己的定义曝光出去了,可能导致命名污染 - :: (作用域限制符)
用 :: 表示出变量(函数等)属于哪块命名空间,如 std::cout
优点:不容易导致命名污染
缺点:麻烦 - 折中
如:using std::cout
每次使用时不用写 std::cout,直接用即可,也没有展开整个 std,也不是很麻烦
☘️例:
using std::cout;
cout << name1::cin << std::endl;
cout << name2::name3::cout << endl;
cin、cout
☘️printf、scanf 和 cout、cin 的区别:
- printf 和 scanf 速度会比 cout 和 cin 快十倍左右,写题时优化速度可以考虑
- cout 和 cin 用起来更方便,可以自动识别类型
- cout 打印 double 默认是五位小数
- .printf 和 scanf 在输入输出时可以指定格式,更加自由
- cin 和 cout 使用 iostream 头文件,printf 和scanf 使用stdio.h
int main() {
//cout
cout << "hello c++!" << endl; //endl 相当于换行(刷新缓冲)
cout << "this is c++" << endl << " learn by yw";
//如果没有 endl 那么会默认追加输出
cout << 'a';
cout << "is";
cout << "a boss"; //a is a boss
//cin
char c;
double b;
cin >> c >> b; //先输入 c 在输入 b,可以不同类型
return 0;
}
缺省参数
☘️声明或定义函数时给函数参数一个默认值,如果调用函数时没有传入实参就使用这个默认值
- 全缺省:函数中所有参数都存在默认值
- 半缺省:函数中只有部分参数存在默认值
int add(int a, int b = -1, int c = 1) {
return a + b + c;
}
❗注意
- 只能从左到右给定函数参数默认值,不然会造成歧义,例:
int mul(int a = 0, int b); //error
//当我们调用 mul(10)时不明确到底将 10 给哪个参数
- 函数声明和定义不能同时都存在默认缺省参数(建议在函数声明时给定默认参数)
int mul(int x = 10, int y = 10); //声明
int mul(int x = 10, int y = 10) { //定义
return x * y;
} //虽然这里编译器不会报错,但是代码运行时会报错
- 缺省值必须是常量或全局变量
☘️函数占位参数
void print(int x) {
cout << "没有占位参数" << x << endl;
}
void print(int x, int) {
cout << "存在占位参数" << x << endl;
}
//两个函数名相同,当我们调用这两个函数时,可以如下:
print(1);
print(1, 2)
//上面两个也可以理解为函数重载(关于占位参数后面还有使用)
函数重载
☘️即函数名相同,功能不同
☘️函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型、个数或者顺序不同
❗注意:函数返回值不可以作为函数重载条件
//函数重载
int div(double x, int y = 1) { //两个整型相除
return x / y;
}
double div(double x, double y = 1) { //两个浮点数相除
return x / y;
}
//函数重载碰到默认参数
void print1(int x) {
cout << "print1(int)" << endl;
}
void print1(int x, int y = 10) {
cout << "print1(int, int)" << endl;
}
//上面两个函数可以发生函数重载,但是:
print1(10); //error,两个函数都能进入,发生歧义
print1(10, 20); //打印结果为 print1(int, int)
//引用作为重载的条件
void func1(int& num) {
cout << "func1(int&)" << endl;
}
void func1(const int& num) {
cout << "func1(const int&)" << endl;
}
const int a = 10;
int b = 10;
func1(a); //func1(const int&)
func1(b); //fun(int&)
//注意:func1(b)只是会优先执行第一个。如果没有第一个函数:
func1(b); //func1 中的 const int&表示不能通过参数 num 改变 b 的值
func1(10); //func1(const int&),引用常量必须要加 const
//注意下列函数不构成重载
int fun(int a, int b) {
return a;
}
int fun(int b, int a) {
return b;
} //参数顺序换了,但参数的类型的位置并没有发生改变
int fun1(int a = 0) {
return a;
}
int fun1(int a) {
return 0;
} //缺省参数不够成函数重载
☘️函数重载的原因
函数在编译时,会生成一个符号表(本质就是一个哈希表)
☘️在 C++ 中:
函数名 | 编译生成的名字 |
---|---|
Add(int a, int b) | _Z3Addii |
Add(double a, double b) | _Z3Adddd |
Add(int a, float b) | _Z3Addif |
☘️C++ 函数符号生成规则如下:
由上我们可以知道:在 C++ 中,重载函数因为参数不同在编译器里的名字就不同
☘️在 C中:
C 编译器生成的符号表就是函数自己的名字,也即函数符号和参数没有关系,只和自己的函数名字有关,自然同名时会产生歧义,所以不支持重载
extern “C”
☘️场景
- gcc 编译器认为 xx.c 文件为 C 程序, xx.cpp 是 C++ 程序;g++ 认为 xx.c 和 xx.cpp 的都是 C++ 程序
- 这就导致了一个问题,如果 test.h 里面有 test.c 的函数声明,用 gcc 去编译 test.c 的文件,文件生成的符号表是 C 的,xx.cpp 去调用 test.h 文件中 C 语言的函数,就会导致编译通过而链接错误。因为链接时找的函数名是 C++ 修饰过的,而生成的符号表是 C 的,以 C++ 的方式寻找肯定找不到
//test.h
#pragma once
#include <iostream>
using namespace std;
int add(int, int);
//test.c
#include "test.h"
int add(int a, int b){
return a + b;
} //test.c 文件使用 gcc 编译,生成的函数符号表是 C 的
//main.cpp
#include "test.h"
int main(){
int a = add(10, 10);
return 0;
}
//在linux中使用下面命令
//gcc -c test.c -o test_o
//g++ -c main.cpp -o main_o
//g++ test_o main_o -o main //error,链接错误
- 所以此时就要告诉 g++ 编译器 test.c 是根据 C 的方式编译的,extern "C"就上场了,它会告诉编译器这部分是按照 C 语言来编译的,所以要修正上面的链接错误
☘️总结
- C++ 编译器通常会对函数名进行名称修饰(name mangling),以支持函数重载等特性,而 C 语言不会进行名称修饰
- 当 C++ 代码需要调用 C 语言编写的函数或使用 C 语言编写的库时,extern “C” 用于确保 C++ 编译器按照 C 语言的链接规则来处理这些函数或变量
☘️例:
//test.c
#include "test.h"
int add(int a, int b){
return a + b;
}
//方法一:在头文件中用extern "C"声明
//test.h
#pragma once
#include <iostream>
#ifdef __cplusplus
extern "C" { //告诉编译器这部分按C语言编译
int add(int a, int b);
}
//extern "C" int add(int a,int b); //这样写也可以
#endif
//方法二:直接在main.cpp文件中使用extern "C"
//main.cpp
#include "test.h"
extern "C" (
int add(int a, int b);
}
☘️补充
如果想要在 C 中使用 C++ 编译的函数,也可以使用 extern “C”,此时 C++ 就会用 C 的方式进行编译
更多关于extern介绍:extern
引用
☘️引用不是新定义一个变量,而是给变量起一个别名,编译器不会给这个变量开辟内存空间,它和它引用的变量共用同一块内存空间
//函数引用传参
void MySwap(int& a, int& b){ //形参的改变可以影响实参
int tmp = a;
a = b;
b = tmp;
}
int main(){
int a = 10, b = 20;
MySwap(10, 20);
cout << a << ' ' << b; //20 10
//注意:引用必须和引用实体是同类型的
return 0;
}
//引用必须初始化
int& a1 = a;
int& d; //error
//一个变量可以有多个引用
int& a2 = a;
int& a3 = a;
//一旦引用一个实体就不能引用其他实体
int& aa = a;
int& aa = b; //error
aa = b; //赋值操作,不会报错
//引用的地址和引用实体的地址相同
cout << &a << endl << &a1; //打印结果相同
//关于常引用的几种情况
//1.隐式转换
double a = 5.5;
int& b = a; //error,不能转换
int a = 4;
double& b = a; //error
//在隐式类型转换时,需要在类型前面加 const
//这时引用的不是变量本身,而是隐式类型转换时的临时空间
int a = 4;
const double& b = a;
cout << &a << "\n" << &b; //打印的两个地址结果不同
//上述情况可以看作如下操作:
const double tmp = a; //临时变量具有常量属性
const double& b = tmp;
//2.权限放大(不行)
const int a = 10;
int& b = a; //error
//3.权限缩小(可以)
int a = 10;
const int& b = a;
//4.权限不变(可以)
const int a = 10;
const int& b = a;
//5.常量引用
int& ref = 10; //error,引用必须是一个合法的内存空间,但10为常量
const int& ref = 10;
//加上const之后,相当于编译器:
//int tmp = 10; const int& ref = tmp;
ref = 20; //error,加入 const 之后就不能修改了
//总结:const type& 可以接收各种类型的对象
//引用传参加上 const 可以防止函数对引用实体修改
//函数引用返回值
int& test(int a, int b){
int c = a + b;
return c; //这里会直接返回 c ,不会生成 c 的拷贝返回
}
int main(){
int a = test(2, 3);
cout << a << endl; //5
return 0;
}
☘️上述代码分析:
- main 函数会将 a 赋以 test 函数中变量 c 的值
- 但是因为 test 函数调用已经结束,c 的空间已经被释放,只是里面的值还是5,所以 a 打印出来的值为 5
- 过程可以理解为如下代码:
int& tmp = c;
int a = tmp;
//函数引用返回值
int& test(int a, int b){
int c = a + b;
return c; //这里会直接返回 c ,不会生成 c 的拷贝返回
}
int main(){
int& a = test(2, 3); //引用接收
cout << a << endl; //5
return 0;
}
☘️上述代码分析:
- 上述代码与前一个大致相同,只是函数 test 的返回值用引用接收
- 过程可以理解为如下:
int& tmp = c;
int& a = tmp; //此时 a 的地址就是变量 c 的地址
//函数传值返回
int test(int a,int b){
return a + b;
}
int main(){
int c = test(3, 2); //5
return 0;
}
☘️上述代码分析:
- return 时会创建一个临时变量 tmp(拷贝返回值),然后将临时变量赋给 c
- 所有的传值返回都会创建临时变量存放返回值,操作如下:
int tmp = a + b;
int c = tmp;
- 临时变量的存放位置:
如果比较小(4 or 8),一般是寄存器充当临时变量
如果比较大,存放在调用函数的栈帧中
关于编译器的保留问题,具体可以看http://t.csdn.cn/7a7M6
☘️引用和指针
引用的本质:是一个指针常量(指针指向不可以修改,但是指向的值可以修改)
int a = 10;
int* const p = &a; //这里的 p 就是指针常量
int& ref = a; //在编译器中自动转换成:int* const ref = &a;
ref = 5; //系统发现是引用,就变成 *ref = 5;
引用的底层实际是指针,在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用一块空间
//引用的大小
long long a = 10;
long long& b = a;
cout << sizeof(b); //8,说明编译器屏蔽了引用和指针的差别
//引用在底层实现上实际是有空间的, 因为引用是按指针方式来实现的
struct test{
char& a;
};
cout << sizeof(test); //结果为4,说明系统也是把引用看成的指针
关于底层引用原理可以参考http://t.csdn.cn/7a7M6
☘️引用和指针的不同点:
- 指针开辟了空间存储地址,引用没有
- 引用必须初始化,指针不用,并且指针可以更改指向
- sizeof 引用是引用类型的大小,而 sizeof 指针是指针变量的大小
- 引用 +1是实体 +1,指针 +1根据指针类型往后偏移
- 访问实体的方式不同,指针需要解引用,引用编译器自己处理
- 引用比指针相对安全
- 有多级指针,没有多级引用
☘️const修饰引用
const修饰引用的作用:禁止通过修改被引用的值来改变引用的对象
int a = 2;
const int& b = a;
b = 10; //error
inline 函数
- inline(内联)函数:编译时会在调用该函数的地方展开
- 作用:inline 函数没有函数压栈的开销,提高了程序运行的效率,一般使用在频繁调用的函数(在 C 语言中,如果频繁调用一个函数,一般会用宏来优化,宏的本质就是替换,会在预处理时进行宏的替换)
☘️inline 函数和宏
宏的缺点:
- 不利于调试,在预处理阶段替换完成了,调试时直接看到替换后的结果
- 可读性差,维护性差,容易出错
- 没有类型安全的检查
宏的优点:
- 代码复用性增强
- 提高了性能(调用函数需要开辟栈帧)
由于宏的不足,C++ 中推荐使用 const、内联函数代替宏
内联函数的优点:
- 不用开辟栈帧
- 在调用处直接展开函数
内联函数使用的注意事项:
- 内联函数本质上是空间换时间,所以代码过长,有循环,有递归等都不适合用
- inline 对编译器只是建议,如果定义 inline 函数里面有循环/递归等,编译器优化时会忽略内联
- 内联函数的声明和定义放一起,不然易出现链接错误(因为被 inline 修饰的函数其链接属性会发生变化,链接属性:inline、external、none)
- 关于 inline 函数的 debug 和 release 版本可以参考:http://t.csdn.cn/7a7M6
inline int Add(int a, int b); //内联函数
int Add(int a, int b){
return a + b;
}
nullptr
- C++98 把 NULL 定义为常量 0,并且将 NULL 作为空指针
#define NULL (void*)0
- C++11 用 nullptr 表示空指针
☘️nullptr 等价于 (void *)0 ?
nullptr 与指针并不等价,nullptr 的类型为 nullptr_t,可隐式转换为任何一种指针类型
☘️相关源码
//关于 nullptr 的源码 stddef.h
#ifdef __cplusplus
namespace std { typedef decltype(__nullptr) nullptr_t; }
using ::std::nullptr_t;
#endif /* __cplusplus */
//这里说明了 nullptr 并不是指针类型,而是 nullptr_t
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
//其中__cplusplus 是一个宏(本质上是一个值)
//上述源码的意思为:如果是 C++,NULL 就给成 0,如果是 C,就给成((void*)0)
☘️实例
//test.cpp
void func(int){
cout << "void func(int)" << endl;
}
void func(int*){ //函数重载
cout << "void func(int*)" << endl;
}
void func2(void*){
cout << "void func(void*)" << endl;
}
int main(){
func(NULL); //void func(int),说明 C++ 把 NULL 看成 0
func(nullptr); //void func(int*)
func2(nullptr); //void func(void*)
//上面两个函数说明 nullptr 可以转换成任何类型指针
nullptr_t x = nullptr; //nullptr_t 类型可以隐式转换为任何一种指针类型
int* p1 = x;
double* p2 = x;
float* p3 = x;
void* p4 = x;
//并且在 C 中 free(NULL)是不安全的
free(NULL); //error
delete x; //不会报错,比较安全
}
//下面的例子也可以说明
template<typename T>
void test(T a){
cout << "普通类型\n";
}
template<typename T>
void test(T* a){
cout << "指针类型\n";
}
int main(){
test((void*)0); //指针类型,说明传入的参数是一个指针
test(nullptr); //普通类型,说明 nullptr 只会在迫不得已才会转变成为指针类型
return 0;
}
☘️总结:建议初始化时空指针使用 nullptr
- nullptr 的类型不是指针,但是在“迫不得已”的情况下会隐式转换成指针,即 nullptr_t 可以转成指针,但是指针不一定可以转化成 nullptr_t
- C++11 会把 NULL 看成 0
- C 语言里的 NULL 是 ((void*)0),一个指针指向内存 0x00000000,这块内存不放有效数据
- 在动态内存管理时 delete nullptr 更加安全
- sizeof(nullptr) = sieof((void*)0) = 指针类型的大小
auto
☘️auto 是一个类型(指示符),auto 可以让编译器在编译时自动推导变量的类型
auto p1 = 1; //编译器在编译时推导出 p1 为 int 类型
cout << sizeof(p1) << endl; //4
//auto 使用的注意事项:
//1. 必须初始化
auto p1; //error
//2. auto 推导指针时 auto 和 auto* 是一样的,但声明引用时必须加 &
int a = 10;
auto ptr = &a;
auto* ptr1 = &a;
auto& tmp = a; //声明引用必须要加上 &
//typeid(变量名).name() 返回的是表示变量类型的字符串
cout << typeid(ptr).name() << endl; //int*
cout << typeid(ptr1).name() << endl; //int*
cout << typeid(tmp).name() << endl; //int
//3. auto 不能用来作函数参数和数组声明(不知道要开辟多大空间)
auto arr[] = { 1,2,3 }; //error
//4. auto 可以同时声明多个变量(变量类型必须相同)
auto a = 1, b = 2;
auto p = 1, p2 = &a; //error
范围 for 循环
int arr[10] = { 9,8,7,6,5,4,3,2,1 };
//常规遍历数据
for (int i = 0; i < sizeof(arr); i++){
cout << arr[i] << ' ';
}
//C++11 中引入了基于范围的 for 循环。for 循环后的括号由冒号“:”分为两部分:
//第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
for (auto e : arr){
cout << e << " ";
} //打印结果为9 8 7 6 5 4 3 2 1
//注意:和普通循环一样也可以用 break 和 continue 语句
//例:将 arr 中的每个元素 *2
for (auto e : arr){
e *= 2;
}
for (auto e : arr){ //打印
cout << e << " ";
}
cout << endl; //打印结果为9 8 7 6 5 4 3 2 1
//这是因为 e*=2 中的 e 是数组元素的一份拷贝,并没有改变数组元素的值
//正确方式
for (auto& e : arr){ //加上引用,操作的就是数组里的元素
e *= 2;
}
☘️范围 for 的使用条件
- for 循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;
- 对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围
void test(int arr[]){
for (auto& e : arr){
cout << e; //error,for 的范围不确定,传参过去的数组不能用范围 for
}
}
本篇文章到这里就结束啦,欢迎批评指正!