个人专栏:《数据结构-初阶》《经典OJ题目》《C语言》《小白算法成长录》
欢迎大佬交流
本文代码已同步GitHub:GitHub - Stellen-z/DailyCode: pracetice · GitHub
一、从C语言到C++
1、为什么学习C++
C++是在C语言基础上发展出来的语言,它不仅保留了C的:
- 高性能
- 直接操作内存的能力
- 适合底层开发的特性
还增加了:
- 面向对象编程(OOP)
- 泛型编程(模板)
- 标准模板库(STL)
简单理解:
C是“工具”,C++是“工具箱 + 设计系统”
学习C++的意义是:
- 从“过程式思维” → “结构化 + 对象化思维”
- 从“写单个函数” → “设计完整程序结构”
- 为后续高阶数据结构与算法打基础
2、C和C++的区别
C语言:面向过程编程
C语言以“函数”为核心,把问题拆解为一个个步骤:
- 程序 = 函数 + 数据
- 强调“怎么做”(过程)
C++:多范式语言(面向对象为主)
C++支持:
- 面向过程(兼容C)
- 面向对象(OOP)
- 泛型编程(模板)
更强调:
“谁来做这件事”(对象)
C语言不支持:
- 没有类(class)
- 没有对象
- 没有封装 / 继承 / 多态
C++支持:
- class / struct(增强版)
- 封装(封装数据和方法)
- 继承(代码复用)
- 多态(接口统一)
3、第一个C++程序
首先明确C++兼容C绝大多数的语法,因此我们先在C++环境中编译C代码:
#include <stdio.h> int main() { printf("Hello World!\n"); return 0; }程序正常运行!
当然,C++有自己的输入输出,肯定不会像C语言这样写
#include <iostream> using namespace std; int main() { cout << "Hello World!" << endl; return 0; }不知道各位小伙伴用C++/Java第一次写出Hello World时,和第一次用C语言写出来的Hello World有什么不同的感觉呢?
下面我们就从C++的第一个程序开始学习基础语法!
二、C++基础语法
1、namespace
我们先来看一段C语言程序:
#include <stdio.h> #include <stdlib.h> int rand = 10; int main() { //error C2365: “rand”: 重定义;以前的定义是“函数” printf("%d\n", rand); return 0; }直接编译错误!
报错原因是重定义,当我们想打印 rand 时,在C语言的编译环境下、
rand是一个函数,而我们在全局变量中又定义了rand,导致rand的重定义!
那么在C++中,我们该怎么避免这种情况呢?
答案就是使用 namespace--命名空间
namespace 的定义
● 定义命名空间,需要使用namespace 关键字,后面跟命名空间的名字,然后加上一对{ },{ }中即为命名空间的成员;可以定义变量、函数、类等;
● namespace 本质是定义一个域,这个域跟全局域各自独立,而不同的域可以定义同名变量,因此基本解决了命名冲突
● C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑;
因此,有了域隔离,名字冲突就解决了;
局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
● namespace 只能定义在全局,当然也可以嵌套定义
● 在项目工程多文件中,定义的同名 namespace 会认为是一个 namespace ,不会冲突
● C++标准库都放在一个叫 std(standard) 的命名空间中
下面我们来定义上述的例子
//1.命名空间定义 #include <stdio.h> #include <stdlib.h> namespace stn { //定义变量 int rand = 20; //定义函数 int Add(int x, int y) { return x + y; } } int main() { printf("%p\n", rand);//默认访问rand函数指针 printf("%d\n", stn::rand);//访问stn命名空间中的rand return 0; }//2.命名空间的嵌套 #include <stdio.h> #include <stdlib.h> namespace mn { //pq命名空间 namespace pq { int rand = 1; int Add(int x, int y) { return x + y; } } //xy命名空间 namespace xy { int rand = 1; int Add(int x, int y) { return x + y; } } } int main() { printf("%d\n", mn::pq::rand); printf("%d\n", mn::xy::rand); printf("%d\n", mn::pq::Add(1, 2)); printf("%d\n", mn::xy::Add(1, 2)); return 0; }//多文件可以重复定义同名的namespace,他们会默认合并到一起 //Stack.h #pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> namespace stn { // 支持动态增长的栈 typedef int STDataType; typedef struct Stack { STDataType* _a; int _top; // 栈顶 int _capacity; // 容量 }Stack; // 初始化栈 void StackInit(Stack* ps); // 入栈 void StackPush(Stack* ps, STDataType data); // 出栈 void StackPop(Stack* ps); // 获取栈顶元素 STDataType StackTop(Stack* ps); // 获取栈中有效元素个数 int StackSize(Stack* ps); // 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 bool StackEmpty(Stack* ps); // 销毁栈 void StackDestroy(Stack* ps); }//Stack.cpp #include "Stack.h" namespace stn { // 初始化栈 void StackInit(Stack* ps) { assert(ps); ps->_a = NULL; ps->_capacity = ps->_top = 0; } // 入栈 void StackPush(Stack* ps, STDataType data) { assert(ps); if (ps->_capacity == ps->_top) { int newcapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity; STDataType* tmp = (STDataType*)realloc(ps->_a, sizeof(STDataType) * newcapacity); if (tmp == NULL) { perror("realloc failed!\n"); exit(1); } ps->_a = tmp; ps->_capacity = newcapacity; } ps->_a[ps->_top++] = data; } // 出栈 void StackPop(Stack* ps) { assert(ps); assert(ps->_top > 0); ps->_top--; } // 获取栈顶元素 STDataType StackTop(Stack* ps) { assert(ps); assert(ps->_top > 0); return ps->_a[ps->_top - 1]; } // 获取栈中有效元素个数 int StackSize(Stack* ps) { assert(ps); return ps->_top; } // 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 bool StackEmpty(Stack* ps) { assert(ps); return ps->_top == 0; } // 销毁栈 void StackDestroy(Stack* ps) { free(ps->_a); ps->_a = NULL; ps->_capacity = ps->_top = 0; } }//Test.cpp #include "Stack.h" //定义全局的Stack typedef struct Stack { int a[10]; int top; }ST; void STInit(ST* ps) {}; void STPush(ST* ps, int x) {}; int main() { //调用全局的栈 ST st1; STInit(&st1); STPush(&st1, 1); STPush(&st1, 2); STPush(&st1, 3); printf("%d\n", sizeof(st1));//固定10个int类型,共44字节 //调用stn的栈 stn::Stack st2; printf("%d\n", sizeof(st2));//一个x64环境下指针,两个int类型,共16字节 stn::StackInit(&st2); stn::StackPush(&st2, 1); stn::StackPush(&st2, 2); stn::StackPush(&st2, 3); }namespace 的使用
当编译器在查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间中查找;
因此下面的程序会报错
#include <stdio.h> namespace stn { int a = 0; int b = 1; } int main() { //error C2065: “a”: 未声明的标识符 printf("%d\n", a); return 0; }要使用命名空间中定义和变量、函数,有三种方式:
a、指定命名空间,项目中推荐这种方式
int main() { printf("%d\n", stn::a); return 0; }b、using将命名空间中某个成员展开,项目中经常访问且不存在冲突的成员推荐这种方式
using stn::a; int main() { printf("%d\n", a); printf("%d\n", stn::b); return 0; }c、展开命名空间全部成员,项目不推荐,冲突风险很大,日常练习可以使用
using namespace stn; int main() { printf("%d\n", a); printf("%d\n", b); return 0; }2、输入输出
在C语言中,包含输入输出的头文件是<stdio.h>
而在C++中,包含输入输出的头文件是<iostream>
<iostream>是 Input Output Stream的缩写,是标准的输入,输出流库,定义了标准的输入,输出对象;
而在前面我们说过,std这个命名空间里面包含cin 和 cout
std::cin 是 istream类的对象,主要面向的是窄字符(narrow characters)的标准输入流,那么宽字符对应的就是wcin;
std::cout 是 ostream 类的对象,主要面向窄字符的标准输出,那么宽字符对应的就是wcout;
<<是流插入运算符,>>是流提取运算符
std::endl 是一个函数,流插入输出时,相当于一个换行符加刷新缓冲区;
使用C++输入输出更方便,不需要printf/scanf那样需要手动指定格式,C++的输入输出可以自动识别变量类型
cout/cin/endl等都属于C++标准库,C++标准库都放在std的命名空间中,因此要通过命名空间来使用
因此,在日常练习中我们可以 using namespace std,实际项目开发中不能直接展开
由于C++兼容C大部分语法,因此在编译时,会一并编译C语言的代码
这就导致有时C++的效率会降低
比如当需要处理大量输入输出时,为了提高效率,我们通常会加上下面这三行代码
#include <iostream> int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); std::cout.tie(nullptr); return 0; }三行代码的作用:
std::ios_base::sync_with_stdio(false);
取消 C++ 标准流(cin/cout)与 C 标准流(stdio)之间的同步;默认情况下两者同步,以保证混用时的顺序正确,但会带来额外开销。关闭同步后,cin/cout效率大幅提升,但不能再混用printf/scanf,否则会导致未定义行为。std::cin.tie(nullptr);
解除cin与cout的绑定。默认情况下,每次执行cin前会自动刷新cout,解除绑定可减少不必要的刷新开销。std::cout.tie(nullptr);
解除cout与其他流的绑定(通常无额外绑定,但保持写法对称或防止未来修改)。
三、函数增强
1、缺省参数
缺省参数的定义:允许在函数声明时为参数指定默认值;调用时若省略该实参,则自动使用默认值。
缺省分为全缺省、半缺省;
全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值;
注:C++规定半缺省参数必须从右向左依次连续缺省,不能间隔跳跃给缺省值
函数定义和声明分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值
我们来看下面的例子就能明白
a、缺省参数的使用
#include <iostream> using namespace std; void fun(int a = 1) { cout << a << endl; } int main() { fun(); //没有传参时,使用参数的默认值 fun(10); //传参时,使用指定的实参 }b、缺省参数的分类
#include <iostream> using namespace std; //全缺省 void Func1(int a = 1,int b = 2,int c = 3) { cout << a << " " << b << " " << c << endl; } //半缺省 void Func2(int a, int b = 2, int c = 3) { cout << a << " " << b << " " << c << endl; } int main() { Func1(); //1 2 3 Func1(10); //10 2 3 Func1(10,20); //10 20 3 Func1(10,20,30); //10 20 30 Func2(100); //100 2 3 Func2(100,200); //100 200 3 Func2(100,200,300); //100 200 300 }2、函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同:可以是参数个数不同或者是类型不同!
注:C语言是不支持同一作用域出现同名函数的
我们通过代码来观察
#include <iostream> using namespace std; namespace stn { //1.参数类型不同 int Add(int x, int y) { cout << "Add(int x, int y)" << endl; return x + y; } double Add(double x, double y) { cout << "Add(double x, double y)" << endl; return x + y; } //2.参数个数不同 void func(int a) { cout << "func(int a)" << endl; } void func(int a, int b) { cout << "func(int a,int b)" << 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 main() { stn::Add(1, 2); stn::Add(1.1, 2.2); stn::func(1); stn::func(1,2); stn::f(1, 'x'); stn::f('x', 1); return 0; }既然参数类型、参数个数、参数顺序都会构成函数重载,那么函数返回值是否会构成重载呢?
我们来试一下
#include <iostream> using namespace std; int f() { cout << "f()" << endl; return 1; } //error C2556: “void f(void)”: 重载函数与“int f(void)”只是在返回类型上不同 void f() { cout << "f()" << endl; } int main() { f(); f(); return 0; }编译失败,编译器在调用时不知道到底调用哪一个函数!
最后我们来看下面这组代码
#include <iostream> using namespace std; void f() { cout << "f()" << endl; } void f(int a = 10) { cout << "f(int a = 10)" << endl; } int main() { f();//error C2668: “f”: 对重载函数的调用不明确 f(10); return 0; }首先来看,这两个函数是否构成函数重载?
答案是构成!
那为什么会报错呢?
当全缺省函数和无参函数都存在时,如果调用函数时没有进行传参,就会引发歧义,编译器不知道到底该调哪个函数!
3、inline
a、内联函数的定义
用 inline 修饰的的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,提高效率!
b、内联函数的使用
下面我们来写一个简单的内联函数
#include <iostream> using namespace std; inline int Add(int x, int y) { int ret = x + y; ret += 1; return ret; } int main() { int ret = Add(1, 2); cout << ret << endl; return 0; }通过调试来观察内联函数是否展开
发现框选部分并没有call/ret指令,即没有通过call指令进行调用,直接将代码复制过来!
最后要注意,inline不建议声明和定义分离到两个文件,分离会导致连接错误;
因为inline被展开,没有函数地址,链接时会出现报错!
c、内联函数与宏函数的区别
C++设计内联函数的目的就是为了替代C语言的宏函数
C语言的宏函数也会在预处理时替换展开,但是宏函数很复杂,并且由于直接替换,不方便调试;
下面我们来简单实现一个加法宏函数
//请问哪个宏函数是正确的? #define Add(int a,int b) return a + b; #define Add(a,b) return a + b; #define Add(a,b) (a + b)答案是上述三个均是错误的宏函数
宏函数的本质是进行替换;
首先来看第一个
//#define Add(int a,int b) return a + b; int ret = Add(1, 2); //展开之后变成 int ret = return 1 + 2;; //1.return不能出现在赋值表达式 //2.末尾有两个分号接着来看第二个
//#define Add(a,b) a + b; int ret = Add(1, 2); //展开之后变成 int ret = 1 + 2;; //此时影响不大,结果正常 cout << Add(1, 2) * 3 << endl; //这样展开之后呢? //cout << 1 + 2 * 3 ; << endl; //显然1.没有括号 2.分号多余继续来看第三个
//#define Add(a,b) (a + b) cout << Add(1, 2) << endl; // -> cout << (1 + 2) << endl; //此时没有问题 cout << Add(1 & 2, 1 | 2) << endl; // -> cout << (1 & 2 + 1 | 2) << endl; //显然错误,+ 的运算符优先级高于 & ,导致计算顺序错误最后来看正确写法
#define Add(a,b) ((a) + (b));四、引用
1、引用的概念和定义
引用的基本概念
引用是C++中一种特殊的变量类型,本质上是已存在变量的别名;
通过引用可以直接操作原变量,无需拷贝数据;
引用在定义时必须初始化,且一旦绑定到某个变量后无法更改指向。
引用的定义语法
引用的定义方式为在变量类型后加 & 符号:
- 类型匹配:引用必须与原变量类型一致(
const引用除外)。 - 必须初始化:定义时需直接绑定到一个已存在的变量。
- 无独立内存:引用不占用额外存储空间,仅作为别名存在。
我们尝试来使用引用
#include <iostream> using namespace std; int main() { int a = 1; int& b = a; int& c = a; int& d = a; cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0; }2、const引用
a、引用const对象必须要用const来引用;const引用也可以引用普通对象,那么就会造成权限缩小的问题;
#include <iostream> using namespace std; int main() { const int a = 1; //权限放大:error C2440: “初始化”: 无法从“const int”转换为“int &” //int& ra = a; //正确引用 const int& ra = a; //error C3892: “ra”: 不能给常量赋值 //ra++; //权限缩小 int b = 10; const int& rb = b; //error C3892: “rb”: 不能给常量赋值 //rb++; return 0; }b、临时对象是指编译器需要一个空间暂存表达式的求职结果时,临时创建的一个未命名对象;
而在类型转换中就会产生临时对象,而临时对象具有常性,此时就需要使用常引用!
#include <iostream> using namespace std; int main() { int a = 10; //常量值用常引用 const int& x = 20; //error C2440: “初始化”: 无法从“int”转换为“int &” //int& ret = a * 2; //临时对象用常引用 const int& ret = a * 2; double d = 11.22; //error C2440: “初始化”: 无法从“double”转换为“int &” //int& rd = d; //类型转换会产生临时对象,要用常引用 const int& rd = d; return 0; }3、指针和引用
引用与指针的区别
- 初始化要求:引用必须初始化,指针可以为空(
nullptr) - 语法概念上:引用是一个变量的别名,不开空间,指针是存储一个变量的地址,要开空间
- 可修改性:引用绑定后不可更改,指针可以重新指向其他地址
- 访问语法:引用直接使用变量名操作,指针需解引用(
*ptr) - 大小上:sizoef中引用的大小取决于引用类型的大小,但指针始终是地址空间所占字节个数
- 安全性:引用不存在“空引用”问题,指针可能悬空
五、空指针
1、NULL
在C语言中,NULL实际上是一个宏;
在stddef.h文件中可以看到下面代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endifC++中NULL被定义成0,C语言中是一个类型为void *、值为零的空指针常量;
我们来看下面代码
#include <iostream> using namespace std; void f(int x) { cout << "f(int x)" << endl; } void f(int* ptr) { cout << "f(int* ptr)" << endl; } int main() { f(0); f(NULL);//调用f(int x) f((int*)0); f((int*)NULL);//调用f(int* ptr) //error C2665: “f”: 没有重载函数可以转换所有参数类型 //f((void*)NULL); return 0; }会发现即使传入NULL,本想调用指针版本的,却仍调用整数版本!导致出现很多歧义;
最后由于C++禁止void*隐式类型转换为其他指针类型,因此会报错
2、nullptr
在C++中,为了避免函数调用歧义的问题,引入了新的关键字 nullptr;
nullptr可以转换成任意其他类型的指针;
因此,使用nullptr可以避免指针类型转换的问题;
nullptr只能隐式的转换成指针类型,而不能被转换成整数类型
如果觉得有帮助,可以关注 GitHub 项目持续更新:GitHub - Stellen-z/DailyCode: pracetice · GitHub