C++17入门经典上
Chapter 1 基本概念
- 头文件包含许多内容,其中包括.cpp文件中的可执行代码使用的函数原型,以及使用的类和模板的定义
- 预处理指令会以某种方式修改源代码,之后会把他们编译为可执行的形式
- 头文件的内容会添加到源文件中
- 头文件的内容被插入到#include指令的位置
- 流是数据源或数据接收器的一种抽象表示,每个流都关联着某台设备,关联着数据源的流就是输入流,关联着目的地的流就是输出流
- 名称空间类似于姓氏
- 两个冒号::有一个非常奇特的名称,作用域解析运算符
- main函数不能定义在名称空间中,未在名称空间中定义的内容都存在于全局名称空间中,全局名称空间没有名称
- 不要使用以下划线开头的名称
- 有时候程序需要几个类似的类或函数,其代码中只有所处理的数据类型有区别,编译器使用模板给特定的自定义类型自动生成类或函数代码
Chapter 2 基本数据类型
- 花括号称为初始化列表,初始化列表可以包含几个值;优势:允许以相同的方式初始化所有变量,被称为统一初始化
- 可以在花括号中使用单个值来初始化任何变量 c++17
int apple_count{12};
int counter{};//零初始化 {}相当于0
const unsigned toe_count{10};//常量
- const 可以固定任何类型的变量值,不可被修改
- unsigned 是无符号数,永远不会是负值
- 整数除法返回的是分母除以分子得到的倍数,任何余数都会被舍弃
- 浮点类型数据不能使用unsigned或signed修饰符,浮点类型总是带符号的
- cmath头文件中的所有函数可接受任意浮点类型或整型参数
- abs(arg)
- ceil(arg)
- floor(arg)
- exp(arg)
- sqrt(arg)
- round(arg)
- pow(arg1,arg2)
- tan(angle)
- iomanip头文件,控制数据格式
- fixed 用小数点固定格式
- dec 十进制格式
- left 左对齐格式
- setprecision(n) 一共n位输出,if有fixed,则表示小数点后n位数字
- setw(n) 输出序列宽度
- setfill(ch) ch填充多余,默认是空格
- static_cast<要转的新类型>(数据) 强制类型转换
- auto 关键字可以告诉编译器应推断数据类型
auto num{10};
Chapter 3 处理基本数据类型
- 枚举 定义:
enum class 类名:指定成员的类型{枚举成员1,...,枚举成员n};//默认枚举成员1 = 0,后一个比前一个大1
// 成员类型可以省略不写
可以当成类来看待,就是一个类名数据类型,引用枚举时,必须使用类型名(类名)来限定他
枚举成员的值必须是编译器可以计算出来的常量表达式,这种表达式包括字面量,以前定义的枚举成员,声明为const的变量,不能使用非const变量(即使使用字面量初始化也不行)
enum class Punctuation : char{Comma=',',Exclamation='!',Question='?'};
- using关键字允许把自己的数据类型名称指定为另一个类型的替代名称
using BigOnes = unsigned long long;
- 全局变量的初始化在main()开始之前进行,默认情况下被初始化为0.局部变量将全局变量隐藏时,使用作用域解析运算符(::)来限定它
Chapter 4 决策
-
cctype头文件
- isupper(c) c是否是大写字母
- isalpha(c) c是否是字母
- isdigit(c) c是否是数字
- isspace(c) c是否是空白,\n \t \r \f ' '
- isblank(c) c是否是空格 ‘ ’ \t‘
- tolower(c) 返回c的小写
- isalnum(c) if c 包含字母或数字,就返回一个正整数(true)else 0(false)
if(变量){} == if(变量!=0) == if(变量!=false) //内! 外==
if(!变量){} == if(变量==0) == if(变量==false)
Chapter 5 数组和循环
数组
- 数组的大小必须用常量表达式来指定
- 数组没有初始化,所以包含的都是垃圾值
double temp[100];
temp[3] = 99.0;
unsigned int height[6];
unsigned int height1[6]{1,2,3,4,5,6};
int height3[6] {};// all 0
const int height4[6]{1,2,3,4,5,6};
- size_t 是某个不带符号的整数类型的别名,足够大,能够容纳编译器支持的任何类型(包括数组)
- 幻数:产量值的多次使用直接定义为const
- 数组大小一般设置为unsigned,一般不会为负值
- std::size(array); 获取数组的大小,c++17
- c++可以使用浮点数来控制for循环
//无限循环中,输入指定个数元素
x[count] = input;
if(++count == size){
cout<<"无法容纳"<<size<<"个元素"<<endl;
break;
}
- 无符号数减去值时候应该小心,0-1=numice_limits<size>::max()
- 字符数组初始化字符串字面量,默认会加上 ’\0‘
char name{"aeiou"};
//6 elements
//因为字符串的最后添加了'\0'来标记字符串结束,所以数组包含6个元素
- 使用数组名不能输出数值类型的数组的内容,这种方法仅仅用于输出char数组,即使是传送给输出流的char数组,也必须用空字符结束,否则程序很可能崩溃
const int maxLength{100};
char text[maxLength]{}
for(int i{};text[i]!='\0';i++){//一个字符数组中的原因个数
if(isalpha(text[i])){
switch(tolower(text[i])){
case 'a':case 'e':case 'i': case 'o' :case 'u':
++vowels;
break;
default:
++consonants;
}
}
}
- 不能使用cin>>直接读取输入内容,因为>>不能碰到空格
- cin.getline(text,maxlength);
- 多维数组和一维的类似,空初始化列表是将其初始化为0
- 多维数组除了第一个纬度之外,必须指定大小,编译器只能推断第一个纬度的大小
- 在运行期间给数组分配内存空间
cin>>count;
unsigned int height[count];
//此时数组无法初始化,因为不知道有多少元素。
//因此这种带有变量个数的数组,只能先声明,后赋值,不可直接初始化
数组的替代品(优于数组)
std::array
array<T,N>
include<array>
- if 创建array<>容器但不指定初始值,则数组也包含垃圾值
- fill()函数把所有元素设置为某个给定的值
array<double,100> values{};
values.fill(3.1415);// all elements to pi
- array.size(); 返回数组的元素个数size_t类型,array对象总是可以通过size()函数确定自己的大小
- array.at(i); 返回i出的值,i是一个索引
- array.front();返回数组第一个值
- array.back();返回数组最后一个值
- 两个array只要容器大小相同,存储类型相同,可以使用== != > < 对两个array中的元素进行逐个比对,也可相互赋值
std::vector<T>
- 大小可自动增加,可容纳任意数量元素
- push_back(X);向vector中添加元素
vector<long> numbers(20,99L);//理解为构造器(),20个 99
vector<int> number(20);//有20个0
vector<long> number{20};//只有一个20
vector<unsigned int> primes{1,2,3,4,4,4};//初始化{}列表
- 与array类似,两个vector也可以比较,但是vector可以元素数量不同的比,字典序比较
- 一个vector给另外一个vector赋值时,被赋值的会被覆盖已经存在的值
- vector可以存储在其他容器中
- vector没有fill()成员,但是提供了assign函数,可用于重新初始化vector<>内容
vector<long> numbers(20,99L);//20个99
numbers.assign(99,20L);//99个20
numbers.assign({99L,20L});//99 和 20
numbers.clear();//清空numbers
numbers.empty()//vector中是空,没有数据吗?返回true orfalse
numbers.pop_back();//删除vector最后一个元素
如果编译时知道元素的准确数量,就是用array<>,如果不知道就是用vector<>
Chapter 6 指针和引用
指针
- 指针是可存储地址的变量。他指向内存中存储了其他值的位置
- 初始化列表为空,所以这个语句把pnumber初始化为等价于0的指针,即不指向任何内容的地址,等价于0的指针写为nullptr
long* pnumber{nullptr};
long *pnumber{nullptr};
- 定义指针时,总是要初始化他,如果还不能为指针提供期望的值,就将其初始化为nullptr
- 最好在单独的代码行上声明指针和普通变量,以避免出现这种混淆。
- 不管指针指向什么类型或大小的数据,指针变量本身的大小始终是相同的,如今的平台上指针变量大小一般是4或8字节
地址运算符&
- &是一个一元运算符,他可以获取变量的地址。
- &运算符可以应用于任何类型的变量,但必须在对应类型的指针中存储地址
- 使用编译器推断指针类型,但是尽量还是使用auto*
auto* number{&height};
- 使用auto*声明的变量只能用指针值初始化,使用其他类型的值初始化,会导致编译错误。
间接运算符
- 将间接运算符*应用于指针,可以访问指针所指向的内存位置的数据,也成为为解引用运算符
int count{};
int* pount{&count};
- *是乘法运算符,间接运算符,还可以应用于声明指针,一般编译器根据上下文分析
- 使用指针的用处
- 动态的为新变量分配内存空间,即可以在程序执行过程中分配
- 使用指针表示法操作存储在数组中的数据,与普通数组表示法完全等效
- 指针可以在函数中访问函数外部定义的大块数据
- 指针是支持多态性起作用的基础
char类型的指针
- 指向char类型的变量,可以用字符串字面量(实际上是const char类型的数组)初始化
- 给未解除引用的char类型指针应用<<插入运算符-----假定这种指针包含以空字符结尾的字符串的地址
- 如果给解除引用的char类型指针应用<<插入运算符---将地址中的单个字符写入cout
const char* pproverb{"A miss is as good as a mail"};
void proj1()
{
const char* pstr1{ "fatty arb" };
const char* pstr2{ "clara bow" };
const char* pstr3{ "lassie" };
const char* pstr{ "your lucky star is " };
cout << "pick a lucky star! enter a number between 1 and 3:" << endl;
size_t choice{};
cin >> choice;
switch (choice)
{
case 1:cout << pstr << pstr1 << endl; break;//your lucky star is fatty arb
case 2:cout << pstr << pstr2 << endl; break;//字符串名字直接输出字符串
case 3:cout << pstr << pstr3 << endl; break;//未解除引用
default:cout << "sorry you haven't got a lucky star." << endl;
}
cout << *pstr1 << endl;//输出首字符(首地址) 解除引用
}
- 指针数组
每个因子都是一个const char* ,一个const char* 指向一个字符串字面量
其中的*pstr[i] 无法进行再赋值,因为其为常量const,无法进行修改
void proj2()
{
const char* pstars[] = {//默认都会加上\0
"fatty","clara","lassie","slim","boris","mae","oliver","greta"
};
//pstars[]数组中的每个元素 会指向一个const char类型的变量(串/字符数组)
cout << "pick a lucky star! enter a number between 1 and " << size(pstars) << endl;
int choice{};
cin >> choice;
if (choice>0 &&choice<size(pstars))
{
cout << "your lucky star is " << pstars[choice - 1] << endl;
}
else
{
cout << "sorry!" << endl;
}
pstars[0] = pstars[1];//数组内部可以相互赋值,就很离谱!!
}//指针数组法
把一个声明从右向左读。
char * const cp; ( * 读成 pointer to指向 )
cp is a const pointer to char
const char * p;
p is a pointer to const char;
const char* my_favorite_star{ "Lassie" };
my_favorite_star = "Mae";//my_favorite_star本身不是const变量, my_favorite_star = pstars[1];
- 指向常量的指针
指针指向的内容不能修改,但可以把指针设置为指向其他内容
const char* pstring{"some text that cannot be changed"};
const int value{20};
const int* pvalue{&value};
int a{ 1 };
pvalue = &a;
value是一个常量,不能修改,pvalue是一个指向常量的指针,可以用于存储value的地址。不能在非const int指针中存储value的地址。
但是可以把非const变量赋给pvalue、
- 常量指针
存储在指针中的地址不能修改。
只能指向初始化时指定的地址。
但是地址的内容不是常量,可以修改,
int data{ 20 };
int* const pdata{ &data };
*pdata = 25;//内容可变
- 指向常量的常量指针
因为存储在指针中的地址和指针指向的内容都被声明为常量,所以两者都不能修改
const float value{3.14};
const float* const pvalue{&value};
指针和数组
- 数组名可以像指针那样操作,输出时,使用非char类型的数组名,就可以得到内存中的地址。
int num[]{ 1,2,3 };
cout << num << endl;
-
指针可进行的运算:
-
加减:(在数组中表现为向后/前移动一位,就是一个类型的字节数)
-
指针+整数=指针
int data[]{1,2,4}; int* pdata{&data[1]}; cout<<* (pdata+1)<<endl;//4
指针-整数=指针
指针+-指针=整数(类型相同,同一数组中)
结果是两个索引的差值
-
比较
-
动态内存分配
- 所有指针都应该初始化,如果指针没有包含合法的地址,就应该总让他包含nullptr
double* pvalue{new double{}};//初始化为0.0
double* pnull{};//初始化为nullptr
delete pvalue;
pvalue = nullptr;
delete 只是将自由存储区的内存释放,变量还可以使用,但是最好先将其置为nullptr
与普通数组不同的是,无法让编译器推断出动态分配数组的维数
double* data{new double[100]};//100个垃圾值
int* num{new int[100]{0}};//100个0
int* one{new int[]{1,2,3}};//报错,无法推断
delete[] data;//释放动态数组的内存
data=nullptr;
- 释放动态数组的内存,使用delete[] 或者delete [] 方括号表示删除的数组
- ->指针选择成员
- 多维动态数组?
void proj6()
{
int rows{ 3 };
int columns{ 3 };
//动态二维数组
//carrots数组是double*指针的一个动态数组,每个double*指针包含一个double数组的地址
double** carrots{ new double* [rows]{} };
for (size_t i = 0; i < rows; i++)
{
carrots[i] = new double[columns] {};
}
for (size_t i = 0; i < rows; i++)
{
delete[] carrots[i];
}
delete[] carrots;
}
- 每个new必须对应一个delete,每个new[] 必须对应一个delete[]
- 在c++程序设计中,尽量不要使用new delete new[] delete[],应该使用vector<> 和智能指针来规避动态内存的一些风险。尽量避免直接操作动态内存。
智能指针
不必使用delete delete[] 运算符释放内存
智能指针不能进行递增或递减,也不能进行算术操作
<memory>头文件
-
三种智能指针
-
unique_ptr<T>
- 这个对象类似于指向T类型的指针,是惟一的。
- 从不会有两个unique_ptr<>对象指向同一地址
- 指向的值被该对象独占
unique_ptr<double> pdata{ new double{999.0} }; cout << pdata << endl;//地址 cout << *pdata << endl;//数值 cout << pdata.get() << endl;//get()函数返回地址 unique_ptr<double> pdata1{ make_unique<double>(999.0) }; auto pdata2{ make_unique<double>(1.9) };// //三个方法相同,推荐最后一种写法,简洁,且可以防止内存泄漏
reset() 将指针重置为nullptr,如果参数不设置值,就是nullptr
-
release() 将智能指针转换为普通指针,注意,将其转化为原始指针之前一定要先保存原始指针再进行释放,否则将出现内存泄漏的情况
const size_t n{ 100 }; unique_ptr<double[]> pvalues{ new double[n] }; auto pvalues1{ new double[n] }; auto pvalues2{ make_unique<double[]>(n) };//动态创建n个元素的数组 cout << pvalues1 << endl;//地址 for (size_t i = 0; i < n; i++) { pvalues2[i] = i + 1; } /*for (size_t i = 0; i < n; i++) { cout << pvalues2[i] << endl; }*/ pvalues.reset();//将指针重置为nullptr double* values = pvalues2.release(); delete values;
-
-
shared_ptr<T>
-
创建shared_ptr<T> 过程更复杂一些,主要是需要维护引用计数,
void proj8() { shared_ptr<double> pdata{ new double{111.1} }; shared_ptr<double> pdata2;//初始化为nullptr pdata2 = pdata; cout << *pdata2 << endl; }
- 复制pdata会增加引用次数,两个指针必须重置或释放,double变量占用的内存才会释放
- 实际使用共享指针的情况通常涉及对象
-
weak_ptr<T>
理解引用
- 引用就是一个别名,可以用作某对象的别名,不可以只声明引用而不对其初始化
- 引用不能修改另一个另一个对象的别名
- 类型名后面&符号表示引用,如果取得引用的地址,结果会是指向原始变量的一个指针
就当做别名去理解,想不清楚就用原名
void proj9()
{
double data{ 3.5 };
double& rdata{ data };
double* pdata1{ &rdata };
double* pdata2{ &data };
cout << (pdata1 == pdata2) << endl;//相等
double* pdata{ &data };
*pdata += 2.5;
double other_data{ 5.0 };
rdata = other_data;
cout << rdata << endl;
}
Chapter 7 操作字符串
-
c++的string头文件定义了string类型,相比ctring头文件中以‘\0’结尾的c字符串更可靠
-
定义string对象的六种方式
- empty
- 字面量
- 字面量切割
- 构造器
- string对象
- string对象切割
string empty;//长度为0,不包含字符的字符串empty
string proverb{"many a mickle make a muckle."};//字面量
string part_literal{"least said soonest mended.",5};//least 字面量切割
string sleeping(6,'a');//6个a 构造器
string sentence{proverb}; //string 对象
string sentence{proverb,0,13};//(begin,num) //string对象的切割
-
c串与string串的转化,(string 字符串的长度不会计算\0,但是有\0存在(不理它))
- string.c_str();
- string.data();
string proverb{"many a mickle make a muckle."};//字面量
const char* proverb_c_str = proverb.c_str();//const char* 类型字符串
//const char* 表示里面的字符是不能变化的,但是proverb_c_str可以变,指别的地方
char* prover_data = proverb.data();//不是const
cout<<proverb.length();
-
string对象的操作
- 赋值
- 字面量赋值
- 串变量赋值
string adj{"hornwogging"};//字面量赋值 string word{"ribbish"}; word{adj};//串变量赋值 adj = "twotiming";
- 连接
- +连接:必须有一个string对象在+的一侧
- string.append(str,begin,end);连接
- string.append(str,'字符');
string word{"this is a string object"} string description{"whipppersnapper"+word}; string compliment{"~~~what a beautiful name...~~~"}; sentence.append(complient,3,22);//what a beautiful time sentence.append(,'!');//!!!
- 数串连接:
to_string(数字)+字符串
double num{ 100.0 }; string name{ "I'm Huang Hongwei.I am " }; cout << name + to_string(num) + " years old" << endl;
- 赋值
-
读入串getline(cin,text)
string text{}; getline(cin, text); cout << text << endl;
-
访问子串
str.substr(be,num)
str.substr(be)
str.substr()//父子相同
-
out_of_range类型
string phrase{ "The higher the fewer." }; string word1{ phrase.substr(4,6) }; cout << word1 << endl; string word2{ phrase.substr(4,100) }; cout << word2 << endl; string word{ phrase.substr(4) }; cout << word << endl; string str{ phrase.substr() }; cout << str << endl;
-
比较字符串
-
字典序比较:
- 前面的字符都相同,看长度,越长越大
- 长度相同,且对应字符相同,则相同
string对象可以存储到容器中,普通的char数组不能存储到容器中。std空间定义了一个非成员函数模板,实现效果与swap(a,b)相同
-
compare()函数
-
obj. compare()函数可以比较该对象
for(size_t i{1};i<names.size();++i) { if(names[i-1].compare(names[i]>0)) { names[i].swap(names[i-1]); sorted = false; } }
-
使用substr()进行比较
string text{ "peter piper picked a peck of pickled pepper. " }; string phrase{ "got to pick a pocket or two." }; for (size_t i = 0; i < text.length()-3; i++) { if (text.substr(i,4)==phrase.substr(7,4)) { cout << "text contains " << phrase.substr(7, 4) << " starting at index " << i << endl; } }
-
-
搜索字符串find()
string sentence{ "manners maketh man" };
string word{ "man" };
cout << sentence.find(word) << endl;
cout << sentence.find("ma")<< endl;
cout << sentence.find("k")<< endl;
cout << sentence.find("x")<< endl;//返回string:npos
- find(串,对象的初始查找点)
cout<< sentence.find("an",1);//从1开始查找,结果是1
cout<<sentence.finde("an",3);//从3开始查找,结果是16,第二次出现an的位置
-
搜索任意字符集合find_first_of() find_last_of()
从头开始 从结尾开始
string text{"hjkld,ddddd ddddd ddd ddd \"}; string operators{",.\""}; cout<<text.find_first_of(operators)<<endl;//返回集合中(有一个匹配就返回)第一个出现的位置 5
-
find_first_not_of() find_last_not_off()
搜索不在字符集合中的字符的位置
cout<<text.find_first_not_of("aeiouAEIOU");//查找第一个不是元音的位置
逆向搜索字符串str.rfind();
string sentence{"manners maketh man"};
string word{"an"};
cout<<sentence.rfind(word);//16 从后向前搜索,index=16是an第一次出现的地方
//从n开始搜索
-
修改字符串
-
插入 :索引位置前插入,充当当前索引insert()
string phrase{ "we can insert a string." }; string words{ "a string into " }; //phrase.insert(14, words);//在14位置插入words cout << phrase << endl; phrase.insert(13, words, 8,5); //words中从8开始的5个字符 插入到对象的13位置 cout << phrase << endl;
-
-
替换
string text{ "we can replace a string" }; text.replace(1, 5, "123456");//在index=1的地方,的5 个字符,替换为123456 cout << text << endl;
-
删除
删除[begin,]之后的len个字符
unsigned begin{ text.find('c') };
int len{ 3 };
cout << text.erase(begin,len) << endl;
将字符串转化为整型
stoi(string)
string s{"12334"};
int i{stoi(s)};
Chapter 8 定义函数
返回类型 函数名 (参数列表)
{
}
- 函数调用中的实参顺序必须对应于函数列表里的参数顺序。函数名和参数列表的组合称为函数的签名。
- 函数体中可以有多个return语句,每个return语句可能返回不同的值。
- 返回类型是void->return ;
- 函数原型=函数声明:定义了函数名,函数的返回值,参数列表;
- 声明一定要放在调用之前。。除非把实现写在写在引用之前。(同c语言)
给函数传递实参
如果指定的函数实参类型不对应参数类型,编译器就会把参数的类型隐式转换为参数类型
按值传送:
实参的变量值或常量值根本不会传送给函数,而是创建实参的副本,把这些副本传给函数。执行完函数之后就废弃副本(不会对原有的值进行更改)
- 给函数传递指针:参数为指针类型时,按值传送机制就会像以前那样运行,但是指针包含另一个变量的地址,此时,指针的副本也包含这样一个副本
#include<iostream>
#include<string>
#include<iomanip>
using namespace std;
double changeIt(double* pointer_to_it);
int main()
{
double it{ 15.0 };
double result{ changeIt(&it) };
cout << "After function execution, it = " << it << "\n Result returned is " << result << endl;
return 0;
}
double changeIt(double* pit)
{
*pit += 10.0;
cout << "within function, *pit = " << *pit << endl;
return *pit;
}
给函数传送数组:
给函数传送数组的地址要比传送数组更高效,(不需要复制许多元素),函数体中的代码可以把表示数组的参数作为指针来看待,即函数体中可以给数组参数使用指针表示法的强大功能
void proj2()
{
double values[]{ 1.0,2.0,3.00,4.0,5.0,6.0,7.0,8.0,9.0,10.0 };
cout << "average = " << average(values, size(values)) << endl;
}
double average(double array[], size_t count)
{
double sum{};
for (size_t i = 0; i < count; i++)
{
sum += array[i];
}
return sum / count;
}
注意事项:
不能通过size()来避免指定count值,数组参数array只是存储数组的地址,并不是数组本身;
如果使用sizeof(array) 将返回数组地址的内存位置的大小,而不是整个数组的大小
可以相互使用数组和指针表示法
double average2(double* array,size_t count)
{
double sum{};
for (size_t i = 0; i < count; i++)
{
sum += *array++; // 效果一样
//sum+= array[i];
}
return sum / count;
}
存在误区:不要向函数传递固定大小的数组
如果只想计算10个数的平均值,
double average10(double array[10])
{
double sum{};
for (size_t i = 0; i < 10; i++)
{
sum += array[i];
}
return sum / 10;
}
这个函数的签名啊虽然合法,但是呢,会给人一种错误的期待
编译器会强制将刚好包含10个元素的数组作为实参传递给该函数
double average10(double array[10])
double average10(double array[])
double average10(double* array)
//等价的三种方式
//指定维数的数组如果本例中只输入一个带有三个元素的数组,那么仍然会使用10个去计算,下标会越界
const 指针参数
只需要访问数组的值,而不需要修改,使用 *const 类型 arrray ** 只读指针
double average(const double* array,size_t count){}
指定指针为const有两个结果,
- 编译器检查函数体中的代码,确保不会试图修改指针所指向的值(读指针)
- 允许用指向一个常量的实参来调用函数
把多维数组传给函数
函数原型:
double yield(const double values[][4],size_t n);
最好不要在原型的第一个纬度上写数字,可以通过编译器自动推断,但是后续的纬度必须写
double yield(const double array[][4], size_t size);
void proj3()
{
double beans[3][4]{
{1.0,2.0,3.0,4.0},
{5.0,6.0,7.0,8.0},
{9.0,10.0,11.0,12.0} };
cout << "yield = " << yield(beans, size(beans)) << endl;
}
//多维数组并不适合使用指针表示法
double yield(const double array[][4], size_t size)
{
double sum{};
for (size_t i = 0; i < size; i++)
{
for (size_t j = 0; j < std::size(array[i]); j++)
{
sum += array[i][j];
}
}
return sum;
}
note:一层数组不要使用size(),编译器无法推断 a[][num]的第一个维数,因为形参只是保存了数组的地址,并不知道数组的纬度情况
按引用传送
对应与引用参数的实参不会复制,引用参数用实参初始化,它是调用函数中该实参的别名
eg: 引用string string&
类型&
对于类类型这种,按值传送实参和包含因用的函数传参是没有区别的,但是按值传送会对值进行复制,造成内存的浪费
-
对比引用与指针
- 最明显的区别:传送指针的时候,先使用&获取一个值的地址,而在函数内需要使用*解引用
- 指针的鲜明特点:可以为nullptr,而引用必须为某个值。因此允许实参为null,就不能使用引用
- 使用引用可以写出更优雅的语法,但是if不看原型,分辨不清是否应用了引用,(和按值传送的时候长的一模一样)
- 一般认为const int& 比 const int *d更好用
在使用指针之前必须测试指针是否为nullptr,而引用一般不需要担心
-
对比输入输出参数
note: 一般不要使用 即作为输入参数,又作为输出参数的变量
- 输入参数:一般两个选择:const引用传送:const string&(建议使用) 按值传送:string (会复制string对象)
- 可以将T值传送给T&和constT&引用,但是只能将const T 值传送给const T&引用(当然其他也可以传送给他,比如T 表示别名)
-
按引用传送数组
优点:不进行复制。能够修改原始值
可以通过传递引用向函数传递精确的多维数组一维的个数
double average10(const double (&array)[10]); //此时可以在函数中使用size()
note: 关于引用的隐式转化问题:
会有一段临时空间实现相互转换,但是double->int时会出现丢失,所以会有报错
void double_it(const double& it) {//针对于这种带const的会涉及底层转换 cout<<it<endl; } int age{19}; double_it(age);//报错
新的const string引用
c++17 #include<string_view>
string_view() 具有常量特性,他只需要指向某个实际对象:string 字符串字面量或其他任何字符数组中存储的字符序列,,初始化和复制string_view 的开销都特别低
-
隐式创建string会复制字符,但是创建string_view则不会
string_view sv{"hjkl"};
string_view 不允许直接将其转化为string,因为底层涉及char[]的转化,必须进行强制类型转化
string_view 的几点注意事项:
- 不完全等同于const string ,没有提供c_str()来转化为一个const char* 的数组。但是提供了data()函数,于此功能等效
- 不能使用+运算符连接string_view 可以先转化成string再连接eg: string{view};
- string_view 可以从c样式的字符数组创建,大小可以任意大
默认实参值
可在函数原型中指定默认实参值,但是要按照优先级进行指定
void proj4()
{
showError();
}
void showError(string messages)
{
cout << messages << endl;
}
从函数中的返回值
可以从函数中返回任意类型的值。主要讨论返回指针和引用中的陷阱
返回指针
在指针返回到调用函数的时,指针指向的变量必须仍然在其作用域中。
警告:不要从函数返回在栈上分配的自动局部变量的地址
- 不在调用程序中的地址
int* larger(int a, int b)
{
//这些地址不在调用程序中,所以都是错误的
if (a > b)
{
return &a;//wrong!
}
else
{
return &b;//wrong!
}
}
- 在调用程序中的地址
int* larger(int* a, int* b)//传来了地址由指针接收
{
if (*a > *b)
{
return a;//来源于调用程序的地址返回,是可以的
}
else
{
return b;//ok
}
}
一个案例,求一组数据的标准化
在.0-1.0之间的一组数
步骤:
-
减去最小值(查找最小值,每个数字 - 最小值)
//查找最小值 const double* smallest(const double data[], size_t count)//不会对最小值进行更改,读指针 {//我只是用这个值而已 if (count==0) { return nullptr; } size_t index_min{}; for (size_t i = 1; i < count; i++) { if (data[index_min]>data[i]) { index_min = i; } } return &data[index_min]; } //使用最小值,调整数组 double* shift_range(double data[], size_t count, double delta) { for (size_t i = 0; i < count; i++) { data[i] += delta; } return data; }
-
每个元素除以max可以将数组映射到0-1(找到max,除以max)
//查找最小值 const double* largest(const double data[], size_t count)//不会对最小值进行更改,读指针 { if (count == 0) { return nullptr; } size_t index_max{}; for (size_t i = 1; i < count; i++) { if (data[index_max] < data[i]) { index_max = i; } } return &data[index_max]; } //除以最大值 double* scale_range(double data[], size_t count, double divisor) { if (divisor == 0) { return data; } for (size_t i = 0; i < count; i++) { data[i] /= divisor; } return data; }
void show_data(const double data[], size_t count = 1, string title = "data values", size_t width = 10, size_t perline = 5);
double* normalize_range(double data[], size_t count);
const double* largest(const double data[], size_t count);//不会对最小值进行更改,读指针
const double* smallest(const double data[], size_t count);//不会对最小值进行更改,读指针
double* scale_range(double data[], size_t count, double divisor);
double* shift_range(double data[], size_t count, double delta);
int* larger(int a, int b);
int main()
{
//proj1();
proj2();
return 0;
}
void proj2()
{
double samples[]{
11.0,23.0,13.0,4.0,
57.0,36.0,317.0,88.0,
9.0,100.,121.0,12.0 };
const size_t count{ size(samples) };
show_data(samples,count,"original values");
normalize_range(samples, count);
show_data(samples, count, "normalized values", 12);
}
void show_data(const double data[], size_t count, string title, size_t width, size_t perline)
{
cout << title << endl;
for (size_t i = 0; i < count; i++)
{
cout << setw(width) << data[i];
if ((i+1)%perline==0)
{
cout << '\n';
}
}
cout << endl;
}
const double* largest(const double data[], size_t count);
const double* smallest(const double data[], size_t count);
double* shift_range(double data[], size_t count, double delta);
double* scale_range(double data[], size_t count, double divisior);
double* normalize_range(double data[], size_t count);
//规范化
double* normalize_range(double data[], size_t count)
{
return scale_range(shift_range(data, count, -(*smallest(data, count))), count, *largest(data, count));
}
//查找最小值
const double* largest(const double data[], size_t count)//不会对最小值进行更改,读指针
{
if (count == 0)
{
return nullptr;
}
size_t index_max{};
for (size_t i = 1; i < count; i++)
{
if (data[index_max] < data[i])
{
index_max = i;
}
}
return &data[index_max];
}
//除以最大值
double* scale_range(double data[], size_t count, double divisor)
{
if (divisor == 0)
{
return data;
}
for (size_t i = 0; i < count; i++)
{
data[i] /= divisor;
}
return data;
}
//查找最小值
const double* smallest(const double data[], size_t count)//不会对最小值进行更改,读指针
{
if (count==0)
{
return nullptr;
}
size_t index_min{};
for (size_t i = 1; i < count; i++)
{
if (data[index_min]>data[i])
{
index_min = i;
}
}
return &data[index_min];
}
//使用最小值,调整数组
double* shift_range(double data[], size_t count, double delta)
{
for (size_t i = 0; i < count; i++)
{
data[i] += delta;
}
return data;
}
返回引用
不要从函数中返回自动局部变量的引用
想要返回对某个实参的非const引用,就不能把参数指定为const
参数不是const,所以不能把字符串字面量用作实参,编译器不允许这么做
在使用类创建自己的数据类型时,引用返回类型是必不可少的
返回类型的推断
类型推断可以节省时间:return 之后的数据类型要明确
auto 不会推断为一个引用类型,而总是推断为一个值的类型,即使将一个引用赋值给auto,值也会被复制。而且值的这个副本不是const,除非使用const auto
要让编译器推断一个引用类型,可以使用auto& 或const auto&
函数体中的静态变量
函数体中的静态变量在第一次执行的 时候回初始化,后续调用的时候不会在初始化,而只是进行操作,变量始终存在于程序中,直到程序结束
普通变量声明带有垃圾值
静态变量初始化会附带0值
内联函数
内联函数的定义通常放在头文件中,该头文件包含在使用该函数的每个源文件中。即使用该函数的每个源文件都应该对其进行引用
inline int larger(int m,int n)
{
return m > n ? m : n ;
}
函数重载
同名,但是参数列表不同即可
参数列表的几种情况很难判断
重载和指针参数
int* 类型处理起来和int[]的参数类型相同
int* p == int p[];
编译器会无视指定数组维数的方法,如果需要指定维数,可以指定array<>或者按照引用传递
重载和引用参数
如下原型
void do_it(stirng number);
void do_it(string& number);
编译器表示很懵逼,do_it(124);到底调用的是谁?
所以不能根据type1 和type1& 来区分数据类型
note: 临时地址可以用{}来初始化const,而不能用{}初始化非const
重载和const参数
带有const参数和没有const的参数唯一的区别就是,为引用定义参数还是为指针定义参数
-
对于基本类型 int和const int 是相同的
//下面两个原型没有区别 long larger(long a ,long a); long larger(const long a ,const long b);//编译器会忽略这个const
-
重载和const指针参数
在两个重载函数中,一个函数的参数是type *(会修改值),另一个函数的参数类型是const type *(一个只读指针,不会修改) 这两个函数是不同的
-
编译器不会把const值传送给非const指针参数的函数
const 值会传送给const指针
const int* larger(const int* a,const int* b); const int num{0}; *larger(&num)
-
一个函数的参数类型是“指向type的指针”,另一个函数的参数类型是“指向type的const指针”
这两个函数就是相同的
long* larger(long* const a,long* const b);//常量a指向long类型(不能指向别的了) const long* larger(const long* const a,const long* const b);//常量a指向常量的long类型
-
重载和const引用参数
不允许在&后面添加const
T& (const常量无法赋值给T)和const T&(可以接受const和非const)是不同的