C++基础入门
- 统一初始化
- 输入输出
- 输入输出符
- 输入字符串
- const与指针
- c和c++中const的区别
- const与指针的关系
- 常变量与指针
- 同类型指针赋值的兼容规则
- 引用
- 引用的特点
- const引用
- 作为形参替换指针
- 其他引用形式
- 引用和指针的区别
- inline函数
- 缺省参数
- 函数重载
- 判断函数重载的规则
- 名字粉碎
- C++编译时函数名修饰约定规则
- 函数模板
- 名字空间:nameplace
- 命名空间的使用
- new/delete
- C++的动态内存管理
- C11的新特性
- 类型推导
- auto的限制
- 可以推导函数返回值
- decltype关键字
- 基于范围的for循环
- 遍历数组(容器)
- 基于范围的for循环
- 指针空值--nullptr
- typedef与using的使用
- string的使用
统一初始化
初始化列表的方法:
int main() {
int a = 10;
int b{ 10 };
int c(10);
int ar[] = { 1,2,3,4,5,6,7,8 };
int arr[]{ 1,2,3,4,6,5,7,8,9 };
return 0;
}
输入输出
输入输出符
输入流:cin (键盘)
输出流:cout (控制台,屏幕)
使用cin和cout标准输入输出时,必须包含 (iostream)头文件以及std标准命名空间。
int main() {
int a;
int b;
cin>>a>>b;//>>提取符
cout<<"a="<<a<<"b="<<b<<endl;//<<插入符 endl<=>"\n"《=>换行
return 0;
}
输入字符串
int main() {
const int n = 128;
char str[n];
cin >> str ;
cout << str<<endl;//输入abc cde输出abc
cin.getline(str,n);
cout << str << endl;//输入abc def输出abc def
cin.getline(str,n,'#');
cout << str << endl;//输入def vyw# ugfua输出def vyw
return 0;
}
getline函数:
1)getline(istream &is,string &str,char delim);
2)getline(istream& is,string &str);
delim终结符 ,遇到该字符停止读取操作,不写默认回车
const与指针
c和c++中const的区别
c中const修饰一个变量,叫做常变量,其本质上还是一个变量,可以修改。而c++中const是一个常量,本质上不能修改。
int main() {
const int a{ 10 };
int ar[a]{ 1,2,3,4 };
int* p =(int *) &a;
*p = 20;
printf("a=%d *p=%d \n", a, *p);
}
int main() {
const int n{ 10 };
int ar[n]{ 1,2,3,4 };
int* p = (int*)&n;
*p = 100;
cout << "n=" << n << "p=" << *p<<endl;
return 0;
}
const与指针的关系
此处存在三种情况如下:
int main() {
int a=10,b=20;
int* p=&a;//普通指针
const int* p1=&a;//控制了解引用不能改变(解引用为常性)
int const*p2=&a;//控制了解引用不能改变(解引用为常性,同上相同)
int * const p3=&a;//控制了指针本身指向(指针变量自身为常性)
const int* const p4=&a;//解引用和指针本身指向都进行控制
}
以上几种写法均正确,只是编译方式不同
常变量与指针
int main() {
const int a=10;
int* p1=&a;//error a本身为常量,此处错误在于可以用*p解引用进行修改相矛盾
const int* p2=&a;//OK const限制了解引用
int * const p3=&a;//error,仅控制了指向,没有控制解引用
const int* const p4=&a;//OK
int *p5=(int*)&a;//OK 不安全
return 0;
}
同类型指针赋值的兼容规则
能力强的指针赋值给能力收缩的指针
int main() {
int a = 10, b = 20;
int* p = &a;
int* s1 = p;
const int* s2 = p;
int* const s3 = p;
const int* const s4 = p;
cout << "*p=" << *p << " *s1=" << *s1 << " *s2=" << *s2 << "*s3=" << *s3 << "*s4=" << *s4 << endl;
return 0;
}
引用
引用的定义
类型& 引用变量名称=变量名称;
这就是引用变量的定义。&和类型结合称之为引用符号,不是取地址,代表别名的意思。
int main() {
int a=10;
int b=a;
int &c=a;//引用定义时必须进行初始化
return 0;
}
引用的特点
- [ ] List item
int main() {
int a=10;
int &b;//error 引用定义时必须进行初始化
int &c=nullptr;//error 不存在空引用
int& d=a;//OK
inr&&e=a;//error 不存在双重引用
}
const引用
int main() {
int a=10;
const int b=20;
int &x=b;//error
const int &x=b;//OK
const int& y=a;//OK
const int &z=10;//OK
}
作为形参替换指针
使用指针替换两个变量值
void swap(int *x,int *y){
int temp;
temp=*x;
*x=*y;
*y=temp;
}
int main() {
int a=10,b=20;
swap(&a,&b);
return 0;
}
使用引用替换两个变量的值
void swap(int &x,int &y){
int temp=x;
x=y;
y=temp;
}
int main () {
int a=10;
int b=20;
swap(a,b);
return 0;
}
其他引用形式
int main() {
int a=10,b=20;
int ar[5]{1,2,3,4,5};
int *p=&a;
int *&rp=p;//引用指针
int &x=ar[0];
int (&br)[5]=ar;//引用数组
return 0;
}
引用和指针的区别
从语法规则上讲,指针变量存储某个实例(变量或对象)的地址;引用是某个实例的别名。
程序为指针变量分配内存区域;不为引用分配内存区域。
解引用是指针使用前要加上“ * ”;引用可以直接使用,不需要解引用
指针变量的值可以发生改变,存储不同实例的地址;引用在定义时就要被初始化,之后无法改变(不能是其他实例的引用)。
指针变量的值可以为NULL,引用不存在空引用。
指针变量作为形参时需要测试他的合法性(判空),引用不需要。
对指针变量使用sizeof得到的是指针变量的大小(4字节),对引用变量使用sizeof的到的是变量的大小。
理论上指针没有级数的限制(二级指针等),但是引用只有一级
++引用与++指针的效果不同:对指针变量++操作,会使指针变量指向下一个实体的地址;不改变实体本身的内容。但是对引用进行++操作就会直接影响到变量的本身。
不可以对函数中的局部变量或对象以引用或者指针的方式返回。
int * fun() {
int a=10;
return &a;
}
int &func_2() {
int a=10;
return a;
}
//以上两种方法均错误
此处我们在执行的过程中可以运行,但是为什么不能作为返回值呢?
当函数被调用时,函数的形参都在栈桢中,当使用指针或者引用作为返回值时,我们返回的都是这个局部变量的地址,但是函数返回时函数的栈桢会被回收,我们返回的这个地址也就会失效。但是我们为什么可以运行呢,因为这个地址还存在上一个栈桢的存留,因此可以进行使用该地址找到我们局部变量的值,如果这个栈桢被骚扰,或者说这个栈桢被一个新的函数调用时作为了另一个函数的栈桢,返回的地址上的值就会被改变,就不是我们所期望的值。因为我们不能确定该栈桢有没有被骚扰,所以不能用指针或者引用作为返回值。
inline函数
当程序执行函数调用时,系统要进行建立栈空间进行保护现场,传递参数,控制程序执行和转移等当一个函数功能简单且执行多次,为了提高效率,直接将函数的代码嵌入到程序中,这个办法会存在两个缺点,一是书写相同代码,二是程序可读性往往没使用函数的好。为了协调好效率和可读性之间的矛盾,c++r提供了另一种方法–内联函数,方法是在函数定义时用修饰词inline。
inline bool IsNumber(char ch) {
return ch >= '0' && ch <= '9' ? 1 : 0;
}
例如该函数,在其面前加上inline关键字,该函数就变成了内联函数,编译期间编译器能够在调用点内联展开该函数。
inline是一种以空间换时间的做法,省去调用函数的开销。但是函数体的代码过长或是递归函数即便加上inline关键字也不会在调用点内以内联展开该函数。
inline对编译器而言只是一个建议,编译器会自动优化。
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
//A.h
#pragma once
inline bool IsNumber(char ch);
//A.cpp
#include"A.h"
inline bool IsNumber(char ch) {
return ch >= '0' && ch <= '9' ? 1 : 0;
}
//main.cpp
#include"A.h"
#include<iostream>
using namespace std;
int main() {
char c = '4';
bool i=IsNumber(c);
cout << i << endl;
}
//无法解析的外部符号 "bool __cdecl IsNumber(char)" (?IsNumber@@YA_ND@Z),函数 _main 中引用了该符号 测试 D:\tulun\测试\main.obj 1
那在什么情况下使用内联函数什么情况下使用普通函数呢?
如果函数的执行开销小于开栈清栈开销(函数体较小),使用inline处理效率高,如果函数执行开销大于开栈清栈开销,使用普通函数方式处理。
内联函数和宏定义的区别:
- 内联函数在编译时展开,代餐的宏在预编译时展开
- 内联函数直接嵌入到目标代码中,带参的宏时简单的做文本替换。
- 内联函数有类型检测,语法判断功能,宏只是替换
缺省参数
一般情况下,函数调用时的参数个数和形参相同,但是为了更方便的使用函数,c++也允许定义具有缺省参数的函数,这种函数调用时,参数个数可以与形参不相同。
缺省参数指在定义时函数时为形参指定缺省值。
这样的函数在调用时,对于缺省函数,可以给出实参,也可以不给出参数值。如果给出实参,将实参传递给形参进行调用,如果不给出实参,则按缺省值进行调用。
缺省参数的函数调用:缺省参数并不一定时常量表达式,可以是任意表达式,甚至可以通过函数调用给出。如果缺省实参时任意表达式,则函数每次被调用时则表达式被要求重新求值。但是表达式必须有意义。
#include<iostream>
int add1(int a=3,int b=6){
return a+b;
}
int main() {
cout<<add1()<<endl;//9
cout<<add1(4)<<endl;//10
cout<<add1(1,2)<<endl;//3
return 0;
}
缺省参数可以有多个,但所有缺省参数必须放在参数,但是所有缺省参数必须放在参数表的右侧,即先定义所有的非缺省参数,在定义缺省参数。这是在函数调用时,参数自左向有逐个匹配,当实参个形参个数不一致时只有这样才不会产生二义性。
多文件结构
//A.h
void fun(int ,int b=23,int c=8000);
//void fun(int ,int =23.int =8000);
//A.cpp
void fun(int a,int b , int c) {
cout << "a=" << a << "b=" << b << "c=" << c << endl;
}
//main.cpp
int main() {
using namespace std;
fun(12);
fun(10, 20);
fun(10, 20, 30);
return 0;
}
运行结果:
习惯上,缺省参数在公共头文件包含的函数声明中指定,不要再函数定义中指定。如果再函数的定义中指定缺省参数值,再公共头文件包含的函数声明中不能再次指定缺省参数值。缺省实参不一定必须是常量表达式,可以是任意表达式。
int my_rand() {
srand(time(NULL));
int ra = rand() % 100;
return ra;
}
void fun(int a, int b = my_rand()) {
cout << "a=" << a << "b=" << b << endl;
}
int main() {
fun(12);
return 0;
}
当缺省实参是一个表达式时 ,在函数被调用时该表达式被求值。(c语言不支持)
函数重载
C语言实现int,double,char类型的比较大小函数。
int my_max_i(int a,int b) {return a>b?a:b;}
double my_max_d(double a,double b) {return a>b?a:b;}
char my_max_c(char a,char b) {return a>b?a:b;}
这些函数都执行了相同的一般性动作,都返回两个形参中的最大值;从用户角度来看,只有一种操作,就是判断最大值。这种词汇上的复杂性不是“判断参数中的最大值”问题本身固有的,二十反应了程序设计环境的一种局限性:在同一个域中出现的名字必须指向一个唯实体(函数体)。
这就要程序员记住每一个函数的名字,函数重载解决了这种问题。
函数重载:在C++中可以为两个或两个以上的函数提供相同的函数名称,只要参数类型不同或者参数类型相同而参数个数不同称为函数重载
int my_max(int a, int b) {
return a > b ? a : b;
}
double my_max(double a, double b) {
return a > b ? a : b;
}
char my_max(char a, char b) {
return a > b ? a : b;
}
int main() {
int ix = my_max(1, 2);
double dx =my_max (12.23, 23.45);
char cx = my_max('a', 'b');
return 0;
}
如上,如果两个参数表中的参数个数或者顺序不同则认为两个函数重载。
判断函数重载的规则
1.如果两个参数的参数相同,但是返回类型不同,会被标记为编译错误,函数的重复声明。
int my_max(int a,int b){return a>b?a:b;}
unsigned int my_max(int a,int b){return a>b?a:b;}
int main() {
int ix=my_max(12,23);
unsigned int=my_max(12.23);//error 返回值为int
return 0;
}
2.参数表的比较过程与形参名无关
int my_add(int a,int b);
int my_add(int x,int y);
3.如果两个函数的参数表中只缺省参数不同,则第二个声明被歧视为第一个的重复声明。
void Print(int*br,int n);
void Print(int* br,int len=10);
4.typedef名为先右数据提供了一个替换名,他并没有创建一个新类型,因此,如果两个参数表的区别只在于一个使用了typedef,而另一个使用了与typedef,而另一个使用了与typedef相应的类型。则该参数表被视为相同的参数列表。
typedef unsigned int u_int;
int Print(u_int a);
int Print(unsigned int b);
5.当一个参数类型又const或colatile修饰时,如果形参是按值传递方式定义,再识别函数声明是否相同时并不考虑修饰符。
void fun(int a) {}
void fun(const int a){}
6.当一个形参类型又const或者volatile修饰时,如果形参定义指针或引用时,在识别函数声明是否是相同时,就要考虑const修饰词。
void fun(int* p) {}
void fun(const int*p) {}
void fun(int& a) {}
void fun(const int &a) {}
7.注意函数调用的二义性,如果在两个函数的参数表中,形参类型相同,而形参个数不同,形参默认值将会影响函数的重载。
void fun(int a) {}
void fun(int a,int b) {}
void fun(int a,int b=10);
8.函数重载解析的步骤如下
- 确定函数调用考虑的重载函数的结合,确定函数调用中实参表的属性,
- 从重载函数集合中选择函数,该函数可以在(给出实参个数和类型)的情况下可以调用函数
- 选择与调用最匹配的函数。
名字粉碎
在c++中可以实现函数重载就是因为名字粉碎技术使得重载函数在编译过程中进行了区别。例如:
_fastcall调用约定在输出函数名前加上一个“@”符号,在函数名后面也是一个“@”符号和其参数的字节数。格式为:@functionname@number
C++编译时函数名修饰约定规则
_cdecl调用约定
1.以“?”表示函数名的开始,后面跟函数名
2.函数名后面以@@YA表示参数表的开始,后跟参数表;
3.参数表以代号表示:
PA–表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复
4.参数表的第一项为该函数的返回值类型,其后一次为参数的数据类型,指针表示在其所指护具类型前;
5.参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
函数模板
为了代码重用,代码就必须是通用的;通用的代码就必须不受数据类型的限制。那么我们可以把数据类型改为一个设计参数。这种类型的程序设计我们称之为参数化程序设计。软件模块由模板构造,包括函数模板和类模板。
函数模板可以用来创建一个通用功能的函数,以支持多种不同形参,简化重载函数的设计。函数模板定义如下:
template<模板参数表>
返回类型 函数名(形式参数表) {
...;//函数体
}
<模板参数表>,尖括号中不能为空,参数可以有多个,用逗号分开,模板参数主要是模板类型参数。
模板类型参数代表一种类型,由关键字class或typenname后加一个标识符构成,在这里两个关键字意义相同,他们表示后面的参数名代表一个潜在的内置或用户设计的类型。
template<class T>
T my_max(T a,T b){
return a>b?a:b;
}
int main() {
my_max(12,23);
my_max('a','b');
my_max(12.34,23,56);
return 0;
}
在编译过程中,根据函数模板的实参构造出独立的函数,这就是模板函数,这个构造过程称之为模板实例化。
名字空间:nameplace
在C++中支持三种域:局部域,命名空间域,类域。命名空间域即使随C++引入的,相当于给一个文件域用花括号把文件的一部分括起来,并以关键字namespace开头给他起名。括起来的部分称为声明块(可以包括类,变量,函数等)。主要为了解决全局名字空间污染问题,即命名冲突。举例:
namespace yhp{
int g_max=10;
int g_min=0;
int my_add(int a,int b){return a-b;}
}
名字空间域可以进行分层嵌套,同样有分层屏蔽作用,同一个工程中允许存在多个相同的明明空间,编译器最终会合成同一个命名空间。
命名空间的使用
1.加命名空间及作用域限定符
int main() {
int a=yhp::my_add(12,23);
printf("%lf n",Primer::pi);
Primer::MAtrix::my_max('a','b');
return 0;
}
2.使用using将名字空间中成员引入
使用using声明可只写一次限定修饰名。usiing声明以关键字using开头,后面是别限定修饰的名字空间成员名:
using yhp::pi;
using Primer::Matrix::my_max;
int main() {
printf("%lf n",Primer::pi);
printf("%f n",pi);
my_max('a','b');
return 0;
}
3.使用using namespace名字空间名称引入
使用using指示符可以一次性的使用空间中的所有成员直接被使用,比using声明方便。
//A.h
namespace yhp {
int my_add(int, int);
int my_sub(int,int);
}
//A.cpp
#include"A.h"
namespace yhp {
int my_add(int a, int b) {
return a + b;
}
int my_sub(int a, int b) {
return a - b;
}
}
//main.cpp
#include"A.h"
#include<iostream>
using namespace std;
using namespace yhp;
int main() {
printf("%d n",my_add(12, 23));
return 0;
}
使用using指示符,标准C++库中所有组件都在一个std的名字空间中声明和定义的,所以使用标准C++库只写一个using指示符:using namespace std;就可以直接使用标准C++库中所有成员。
new/delete
可执行空间的虚拟地址空间:
内核:操作系统
栈区:函数的形参,非静态的局部变量,函数现场保护数据等等 栈是向下增长的。
共享库的内存映射区域:用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程通信。
堆区:用于程序运行时动态分配内存,堆时可以向上增长的。
数据段:存储全局数据和静态数据。
代码段:可执行的程序和常量数据。
C语言的动态内存管理用malloc,calloc,realloc等函数。
int* ipa=(int*)malloc(sizeof(int)*n);
int *ipb=(int*)calloc(n,sizeof(int));
ipa=(int*)realloc(ipa,sizeof(int)*n*2);
C++的动态内存管理
1.new的运算符使用
int main() {
int n=10;
int* ipa=new int(10);//直接进行赋值
int* ipb=new int[3]{1,2,3};
int* ipc=new int[n]{1,2,3,4,5,6,7};//数组进行赋值
delete ipa;
delete[]ipb;//释放
delete[]ipc;
return 0;
}
2.new的函数方式使用
int main() {
int n = 10;
int* ipa = (int*)::operator new(sizeof(int));
int* ipb = (int*)::operator new(sizeof(int) * n);
::operator delete(ipa);
::operator delete(ipb);
return 0;
}
3.定位new(placement newexpression)的使用
int main() {
int n=10;
int* ipa=(int*)malloc(sizeof(int));
int* ipb=(int*)::operator new(sizeof(int)*n);
new(ipa)int(20);
new(ipb)int[]{1,2,3,4,5,6,7,8,9};
free(ipa);
::operator delete(ipb);
return 0;
}
4.对于内置类型new/delete/malloc/free可以混用。
区别:
- new/delete是C++中的运算符。malloc/free是函数。
- malloc申请内存空间时,手动计算所需大小,new只需类型名,自动计算大小;
- malloc申请的内存空间不会初始化,new可以初始化;
- malloc的返回值为void*,接收时必须强转,new不需要;
- malloc申请内存空间失败时,返回的是NULL,使用时必须判空;new申请内存空间失败时抛出异常,所以要有捕获异常处理程序。
C11的新特性
类型推导
auto类型推导:auto定义的变量,可以根据初始化的值,在编译时推导出变量名的类型。
int main() {
auto x=5;//x为int类型
auto pi=new auto(1);// pi被推到为int*
const auto *xp=&x,u=6;//xp是const int*类型,u是const int类型
static auto dx=0.0;//double
auto int b;//C11中auto不再表示存储类型指示符
auto s;//error 没有初始化 auto无法推导出s的类型
}
int main() {
auto x=5;
const auto *xp=&x,u;//error
const auto* ip=&x,u=6.0;//error
return 0;
}
1.虽然经过前面const auto* xp=&x推导,auto的类型可以确定为int了,但是仍要写后面的=6,否则编译器不予通过。
2.u的初始化不能使编译器推导产生二义性。
auto的一些使用方法可以和同指针,引用结合起来使用,还可以带上cv限定。
1)当不声明为指针或引用时,auto的推到结果和初始化表达式抛弃引用和cv限定后类型一致.
2)当声明为指针或引用时,auto的推导结果将保持初始化表达式的cv属性。
auto不能作为函数形参类
auto的限制
- C11中auto称为类型指示符
- auto不能用于函数参数
- auto不能用于非静态成员变量
- auto无法定义数组
- 实例化模板时不能使用auto作为模板参数
可以推导函数返回值
template<class T>
T my_max(T a,T b){
return a>b?a:b;
}
int main() {
auto x=my_max(12,23);
auto y=my_max('a','b');
cout<<x<<" "<<y<<endl;
return 0;
}
decltype关键字
auto所修饰的变量必须被初始化,即必须要定义变量,若仅希望得到类型不需要定义变量应该怎么办呢?C++11新增了decltype关键字:decltype(exp),exp表示一个表达式,器推导过程是在编译器完成的,不会真正的计算表达式的值。
int main() {
int x=10;
decltype(x) y=1;//y->int
const int &i=x;
decltype(i) j=y;//j->const int &
decltype(x+y) z=0;//z->int
const decltype(z)*ip=&z;//*ip->int,ip->int*
}
也可以用于函数表达式,但不会真正计算表达式的值。
基于范围的for循环
不同容器和数组,遍历的方法不尽相同,写法不同意,也不够简洁,而C++11基于范围的fpr循环以统一,简洁的方式来遍历容器和数组,用起来更加方便了。
遍历数组(容器)
int main(){
int arr[]={1,2,3,4,5};
int len=sizeof(arr)/sizeof(arr[0]);
int* ip=NULL;
for(ip=arr;ip!=ip+len;ip++){
cout<<*ip<<endl;
}
}
基于范围的for循环
int main() {
int arr[]={1,2,3,4,5};
for(int x:arr){
cout<<x<<" ";
}
return end;
return 0;
}
模板:
for(ElenType val:array){
...//循环体
}
//ElenType:是范围变量的数据类型。他必须是数组元素相同的数据类型,或者数组元素可以自动转换过来的类型。
//val:是范围变量的名称。
//array:是让该循环体进行处理的数组名称
//循环体中可以用continue结束本次循环,也可以用break跳出循环
可以使用auto自动推导出val类型
int main() {
int ar[]={1,2,3,4,5};
for(auto x:ar){
x+=10;
cout<<x<<" ";
}
}
指针空值–nullptr
- nullptr是C11新引入的指针控制类型的常量,在C++中可以直接使用。
- sizeof(nullptr)与sizeof((void*)0)所占字节数相同都(4,8)。
- 为了提高代码的健壮性,在后续表示指针空值最好用nullptr。
typedef与using的使用
typedef unsigned int uint;
using uint=unsigned int;
template<class _Ty>
using pointer=_Ty*;
int main() {
int x=10;
double dx=12.25;
pointer<int>ip=&x;
pointer<double>dp=&dx;
return 0;
}
string的使用
int main() {
char str[] = { "tulun" };
int len = strlen(str);
string s1 = "tulun";
for (int i = 0; i < len; ++i) {
cout << str[i];
}
for (int i = 0; i < s1.size(); ++i) {
cout << s1[i];
}
cout << s1 << endl;
s1 = "hello";
cout << s1 << endl;
s1 += "tulun";
cout << s1 << endl;
cin >> s1;
cout << s1 << endl;
return 0;
}
运行结果: