c++入门
1.基础的C++程序编写
1.1:第一个程序
#include<iostream>
using namespace std;
int main(){
cout<<"hello world!"<<endl;
return 0;
}
1.2:C++与C语言第一点区别(输入,输出和头文件)
关于C语言中
#include<stdio,h>
和c++中#include<iostream>
的异同请查看https://blog.csdn.net/weixin_36250487/article/details/80293539
1.cin: 表示标准输入(standard input)的istream类对象。通过 cin 往程序中写入数据
2.cout : 表示标准输出(standard output)的ostream类对象。通过cout 从设备输出或者写数据。
2: 命名空间
细心的你会发现第一个c++程序中有一句
using namespace std;
了吧。
其原因为:所谓namespace,是指标识符的各种可见范围。C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。
2.1引入namepase 的目的
解决命名冲突的问题
在C语言中不能出现两个名字相同的变量或者函数,因此C++引入命名空间的目的就是解决命名空间冲突的问题
#include<iostream>
using namepase std;
namespase A{
int a=10;
}
namespace B{
int a=20;
}
int main(){
cout<<A::a<<endl;
cout<<B::a<<endl;
return 0;
}
该程序输出的两个a值是不同的,就像这样引入命名空间后你想要使用那个a值只需要用作用域限定符 :: 告诉编辑器即可。
2.2:命名空间的使用
1.加命名空间名称及作用域限定符
int main()
{
printf("%d\n", A::a);
return 0;
}
2.使用using 将命名空间中成员引入
using A::a;
int main()
{
printf("%d\n", A::a);
return 0;
}
3.使用using namespace 命名空间名称引入
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
4.其他用法
namespace N1{
int a = 10;
int b = 20;
int Add(int a, int b){
//命名空间中可以有函数的存在
return a + b;
}
namespace N2{
//命名空间中可以出现嵌套式的命名空间
//N1::N2::swap(x,y);
int Swap(int x, int y){
return x*y;
}
}
}
命名空间总结见:
https://blog.csdn.net/jack_wang128801/article/details/90272870
3.缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参
就像是备用钥匙,只有我没有带钥匙的时候才用到缺省参数(对,他就是个备胎)
void TestFunc(int a = 0)
{
cout<<a<<endl;
}
int main()
{
TestFunc(); // 没有传参时,使用参数的默认值
TestFunc(10); // 传参时,使用指定的实参
}
3.1全缺省参数
void Date(int year=2019,int month=6,int day=3){
cout<<"year= "<<year<<endl;
cout<<"month= "<<month<<endl;
cout<<"day= "<<day<<endl;
}
3.2半缺省参数
void Date(int year,int month=3,int day=6){}
//【注意】半缺省参数必须从右往左来给出,不可以间隔给出
// 缺省参数不能在函数声明和定义中同时出现
//缺省值必须是常量或者全局变量
【总结】:所谓的缺省参数是指形参是否被赋予初始值
4.函数重载
4.1 概念:c++允许在同一作用域中声明几个功能相似的几个同名函数,这些函数的形参列表(参数的个数或类型或顺序)必须不同。
常用来处理实现功能类似数据类型不同的问题
//函数重载实例
#include<iostream>
#include<windows.h>
using namespace std;
int Add(int a,int b){
return a + b;
}
double Add(double a, double b){
//相较第一个而言形参的类型不同
return a + b;
}
double Add(double a, int b, float c){
//相较第一个而言形参的类型不同并且有参数个数的不同
return a + b + c;
}
int main(){
int a = 10;
int b = 20;
double c = 2.0;
double d = 3.0;
float f = 3;
cout<<Add(a,b)<<endl;
cout << Add(c, d) << endl;
cout << Add(d, a, f) << endl;
system("pause");
return 0;
}
//c语言中函数名相同会提示重定义错误
[小结]:构成函数重载时参数需满足的条件
1.函数参数个数不同
2.参数类型不同
3.参数顺序不同
【注】:函数返回值类型不能作为函数重载的标记
4.2名字修饰
在c/c++中,一个程序要运行需要经历:预处理、编译、汇编、链接等步骤
- 4.2.1 预处理:去注释,宏替换,头文件展开
- 4.2.3 编译:汇编代码————>机器码
- 4.2.3 汇编:语法检查,转换成汇编代码
- 4.2.4 链接:生成可执行文件
4.3 在Linux下的修饰
void F1(int a);
——Z2F1I
其中—Z为前缀,2代表函数名有两个字符,F1为函数名,而i(int)为参数的类型的首字母
void F1(char b);
//_Z2F1C
void F1(int a,char b);
//_Z2F1IC
【小结】:在Linux系统下函数的编译按一下几个步骤分块
1.前缀_z
2.函数名的字符个数
3.函数的名字
4.函数参数列表中参数类型的首字母(所有参数的首字母)
4.3 C语言的名字修饰规则非常简单,只是在函数名字前面加一个下划线(_函数名),
- 而在C++文件(xx.cpp)中在函数名字前面加一个 extern "c"既是告诉编辑器该函数按照c语言规则来编译
extern "c" int Add(int a,int b)
//一个函数的形式
// 若需要全部都用C语言的风格来编辑,形式如下
extern "c"{
//很多行C语言代码;
}
- 函数重载总结
- 1 C语言不支持,C++支持
- 2 特点:函数名相同,参数不同(1.类型不同。2.顺序不同 3.参数个数不同)
- 3 C++语言支持重载:函数名修饰规则 name mangling
- 4 C语言中底层函数名:_ 函数名
- 5 C++底层函数名: 前缀+函数名+参数类型首字母
5:引用
5.1 引用概念:
引用不是重新定义了一个变量,而是给已经存在的变量取个别名,编译器不会为引用变量开辟内存空间。它和它引用的变量公用同一块内存空间
- 示例代码如下:
- 引用格式:类型名& 引用变量名(对象名) = 引用对象
#include<iostream>
#include<windows.h>
using namespace std;
void Testfun(){
int a = 10;
int &ra = a;
//int b=20;
cout << ra << endl;
//结果出现一个10
cout << ra <<endl<< a<<endl;
//结果出现两个10
}
int main(){
Testfun();
system("pause");
return 0;
}
//ra=b 不是引用,表达式含义是将变量 b 的值放到 a
//&ra = b 不是引用,单独的&ra表示ra取地址
- 监视结果:&ra与&a的值相同,ra与a值都为10;
【注意】:定义的引用类型必须与被引用对象的类型相同
如上例中如果出现 double &ra=a; 则会报错
5.2 引用特性
-
1.引用在定义时必须初始化
-
错误示例: int &ra;
-
2.一个变量可以有多个引用
//正确用法示例:
int a=10;
int &ra=a;
int &rra=a;
int &rrra=a;
- 3.一个引用变量只能指向一个指定的引用对象.(例如;黑旋风只能是李逵,不能是其他人)
5.3 常引用
void TestContRef(){
const int a=10;//常引用
//int & ra=1; //该语句编译时出错,a为常量
const int &ra=a; //常引用
//int& b=10; //该语句编译时出错,b为常量
const int& b=10; //常引用
double d=12.34;
const int& rd=d;//常引用(rd指向的是隐式类型转换时生成的临时变量)
int c=d; //隐式类型转换--->临时变量具有常性
//int& rd=d; //该语句编译时出错,类型不同
}
5.4 使用场景
- 1. 做参数
- 传指针和传引用效果一样;
示例代码:
#include<iostream>
#include<stdlib.h>
using namespace std;
void swap(int *pa, int *pb){
int temp=*pa; //pa的地址与主函数中x地址相同
*pa = *pb; //pb的地址与主函数中x地址相同
*pb = temp;
//交换的是两个参数原来地址空间中的值,
//既原函数中定义的变量x,y的地址空间中的值被交换了
}
//void swap(int &a, int &b){
// int temp = a;
// a = b;
// b = temp;
// //交换的是两个参数原来地址空间中的值,
// //既原函数中定义的变量x,y的地址空间中的值被交换了
//}
void swap(int a, int b){
int temp = a;
a = b;
b = temp;
//交换的是临时拷贝创建的空间里的值
//其原地址空间内的值并没有被交换
}
int main(){
int x = 10;
int y = 20;
swap(&x, &y);
swap(x, y);
system("pause");
return 0;
}
【小结】:传指针与传引用的效果是一样的(都是与原来的变量的地址空间打交道),而传值时会发生临时拷贝(代价太大),所以相比较而言传指针和传引用的效率比较高
- 2.做返回值
#include<iostream>
#include<windows.h>
using namespace std;
int& Testfuc(int &a){
a+=10;
return 0;
}
int main(){
int a = 10;
int &ra=Testfuc(a);
system("pause");
return 0;
}
//此程序中的&ra和&a都指向变量a的地址
【注意】:如果函数返回时,离开函数作用域后,其栈上空间已经还给了系统,因此不能用函数栈帧上的空间做为引用类型的返回值,如果函数类型返回,返回值的生命周期必须不受函数的限制(既函数周期比较长)
#include<iostream>
#include<windows.h>
#include<time.h>
using namespace std;
int main(){
size_t beginl = clock();
for (int i = 0; i < 10000; i++){
cout << "i=" << i << endl;
}
size_t endl = clock();
cout << (endl- beginl) /CLOCKS_PER_SEC<< endl;
system("pause");
return 0;
}
//以毫秒的形式输出循环的执行时间
//引用传值的效率高于值拷贝的形式
5.6 指针与引用的区别
- 引用语法层面:和指针指向同一块内存空间,引用本身没有开辟新的空间
- 引用底层实现:引用开辟新的空间,和指针的的实现相同
int a=10; int a=10;
mov dword ptr [a], OAh mov dword ptr [a], 0Ah
int& ra = a; int* pa = &a;
lea eax, [a] lea еах,[a]
mov dword ptr [ra], eax mov dword ptr [pa], eax
//指针的解引用和引用的解引用底层实现是一样的
ra = 20; *pa = 20;
mov eax, dword ptr [ra] mov eax, dword ptr[ра]
mov dword ptr [eax], 14h mov dword ptr [eax], 14h
引用和指针的不同点:
1. 引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3. 没有NULL引用,但有NULL指针
4在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
.6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8.引用比指针使用起来相对更安全 (引用使用时必须初始化)
6.内联函数
6.1 概念:
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
inline Add(int a,int b){
return a+b;
}
int main(){
int ret=Add(1,3) ------->//若是内敛函数的话,函数体直接展开成int ret=1+3;
//如果不是内联函数的话,底层实现会调用add的函数栈帧call add(0X001)
return 0;
}
6.2:对于普通函数和内联函数的比较
#include<iostream>
using namespace std;
int add(int a,int b){
return a+b;
}
inline int add(int a,int b){
return a+b;
}
int main(){
int ret=add(1,3)
return 0;
}
普通函数
int add(int a,int b){
return a+b;
}底层实现:普通函数,编译时调用add的函数栈帧
push 3
push 1
call add(0821042h)
内联函数
inline int add(int a,int b){
return a+b;
}底层实现:内联函数,编译时函数体展开
mov eax ,2
00012220 add
00012223 mov dword ptr[ret] ,eax
6.3 inline函数的特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
面试题:
- 【面试题】宏的优缺点?
优点:
- 1.增强代码的复用性。
- 2.提高性能。
缺点:
- 1.不方便调试宏。(因为预编译阶段进行了替换)
- 2.导致代码可读性差,可维护性差,容易误用。
- 3.没有类型安全的检查 。
C++有哪些技术替代宏
- 1. 常量定义 换用const
- 2. 函数定义 换用内联函数
7.关键字auto
- 特点:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化
- 表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
#include<iostream>
#include<windows.h>
using namespace std;
int main(){
int a = 10;
auto b = a;
double c = 20.3;
auto d = c;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
system("pause");
return 0;
}
//显示出了类型的名称
7.1. auto与指针和引用结合起来使用用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x; //定义了一个引用
auto* b = &x; //(int *)
auto& c = x; //推导出引用变量的类型
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
7.2 在同一行定义多个变量
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,
- 为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
- 若同一个程序中同时定义了多个 auto ,其会根据第一个变量表达式去推导类型。
7.3 auto不能推导的场景。
7.3.1 auto不能做函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
7.3.2 auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
7.3.3为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
7.3.4 auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
7.3.5 auto不能定义类的非静态成员变量
7.3.6实例化模板时不能使用auto作为模板参数
8 基于范围的for循环(C++11)
8.1 for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
#include<iostream>
#include<windows.h>
using namespace std;
int main(){
int arr[] = {1,2,3,4,5,6,7,8,9};
int size = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (; i < size; i++){
cout << "arr1[i]=" << arr[i] << endl;
}
cout << endl;
for (int e : arr){
cout << "arr2[i]=" << e << endl;
}
cout << endl;
for (auto& e : arr){
cout << "arr3[i]=" <<e << endl;
}
system("pause");
return 0;
}
【注意】:与普通循环类似,基于范围的for循环可以用continue来结束本次循环,也可以用break来跳出整个循环
8.2 范围for的使用条件
1. for循环迭代的范围必须是确定的
- 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提
- 供begin和end的方法,begin和end就是for循环迭代的范围。
- 注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
9.指针空值nullptr(C++11)
#include<iostream>
#include<windows.h>
using namespace std;
int main(){
int *p = NULL;//c++中此处的NULL被定义成常数 "0"
int *pp = nullptr;
cout << typeid(pp).name() << endl;
cout << typeid(nullptr).name() << endl;
system("pause");
return 0;
}
结果如下:
//int *
//std::nullptr_t
程序本意是想通过f(NULL)调用指针版本的f(int)函数,但是由于NULL被定义成 0,因此与程序的初衷相悖。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。**
9.2 nullptr 与 nullptr_t
nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中
typedef decltype(nullptr) nullptr_t;
【注意:】
1 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。