2020-12-25

C++17入门经典上

Chapter 1 基本概念

  1. 头文件包含许多内容,其中包括.cpp文件中的可执行代码使用的函数原型,以及使用的类和模板的定义
  2. 预处理指令会以某种方式修改源代码,之后会把他们编译为可执行的形式
  3. 头文件的内容会添加到源文件中
  4. 头文件的内容被插入到#include指令的位置
  5. 流是数据源或数据接收器的一种抽象表示,每个流都关联着某台设备,关联着数据源的流就是输入流,关联着目的地的流就是输出流
  6. 名称空间类似于姓氏
  7. 两个冒号::有一个非常奇特的名称,作用域解析运算符
  8. main函数不能定义在名称空间中,未在名称空间中定义的内容都存在于全局名称空间中,全局名称空间没有名称
  9. 不要使用以下划线开头的名称
  10. 有时候程序需要几个类似的类或函数,其代码中只有所处理的数据类型有区别,编译器使用模板给特定的自定义类型自动生成类或函数代码

Chapter 2 基本数据类型

  1. 花括号称为初始化列表,初始化列表可以包含几个值;优势:允许以相同的方式初始化所有变量,被称为统一初始化
    1. 可以在花括号中使用单个值来初始化任何变量 c++17
int apple_count{12};    
int counter{};//零初始化 {}相当于0
const unsigned toe_count{10};//常量
  1. const 可以固定任何类型的变量值,不可被修改
  2. unsigned 是无符号数,永远不会是负值
  3. 整数除法返回的是分母除以分子得到的倍数,任何余数都会被舍弃
  4. 浮点类型数据不能使用unsigned或signed修饰符,浮点类型总是带符号的
  5. cmath头文件中的所有函数可接受任意浮点类型或整型参数
    1. abs(arg)
    2. ceil(arg)
    3. floor(arg)
    4. exp(arg)
    5. sqrt(arg)
    6. round(arg)
    7. pow(arg1,arg2)
    8. tan(angle)
  6. iomanip头文件,控制数据格式
    1. fixed 用小数点固定格式
    2. dec 十进制格式
    3. left 左对齐格式
    4. setprecision(n) 一共n位输出,if有fixed,则表示小数点后n位数字
    5. setw(n) 输出序列宽度
    6. setfill(ch) ch填充多余,默认是空格
  7. static_cast<要转的新类型>(数据) 强制类型转换
  8. auto 关键字可以告诉编译器应推断数据类型
auto num{10};   

Chapter 3 处理基本数据类型

  1. 枚举 定义:
enum class 类名:指定成员的类型{枚举成员1,...,枚举成员n};//默认枚举成员1 = 0,后一个比前一个大1
//           成员类型可以省略不写

可以当成类来看待,就是一个类名数据类型,引用枚举时,必须使用类型名(类名)来限定他

枚举成员的值必须是编译器可以计算出来的常量表达式,这种表达式包括字面量,以前定义的枚举成员,声明为const的变量,不能使用非const变量(即使使用字面量初始化也不行)

enum class Punctuation : char{Comma=',',Exclamation='!',Question='?'};
  1. using关键字允许把自己的数据类型名称指定为另一个类型的替代名称
using BigOnes = unsigned long long;
  1. 全局变量的初始化在main()开始之前进行,默认情况下被初始化为0.局部变量将全局变量隐藏时,使用作用域解析运算符(::)来限定它

Chapter 4 决策

  1. cctype头文件

    1. isupper(c) c是否是大写字母
    2. isalpha(c) c是否是字母
    3. isdigit(c) c是否是数字
    4. isspace(c) c是否是空白,\n \t \r \f ' '
    5. isblank(c) c是否是空格 ‘ ’ \t‘
    6. tolower(c) 返回c的小写
    7. isalnum(c) if c 包含字母或数字,就返回一个正整数(true)else 0(false)
  2. if(变量){} == if(变量!=0) == if(变量!=false) //内! 外==

  3. if(!变量){} == if(变量==0) == if(变量==false)

Chapter 5 数组和循环

数组

  1. 数组的大小必须用常量表达式来指定
  2. 数组没有初始化,所以包含的都是垃圾值
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};
  1. size_t 是某个不带符号的整数类型的别名,足够大,能够容纳编译器支持的任何类型(包括数组)
  2. 幻数:产量值的多次使用直接定义为const
  3. 数组大小一般设置为unsigned,一般不会为负值
  4. std::size(array); 获取数组的大小,c++17
  5. c++可以使用浮点数来控制for循环
//无限循环中,输入指定个数元素
x[count] = input;
if(++count == size){
    cout<<"无法容纳"<<size<<"个元素"<<endl;
    break;
}
  1. 无符号数减去值时候应该小心,0-1=numice_limits<size>::max()
  2. 字符数组初始化字符串字面量,默认会加上 ’\0‘
char name{"aeiou"};
//6 elements
//因为字符串的最后添加了'\0'来标记字符串结束,所以数组包含6个元素
  1. 使用数组名不能输出数值类型的数组的内容,这种方法仅仅用于输出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;
        }
    }
}
  1. 不能使用cin>>直接读取输入内容,因为>>不能碰到空格
  2. cin.getline(text,maxlength);
  3. 多维数组和一维的类似,空初始化列表是将其初始化为0
  4. 多维数组除了第一个纬度之外,必须指定大小,编译器只能推断第一个纬度的大小
  5. 在运行期间给数组分配内存空间
cin>>count;
unsigned int height[count];
//此时数组无法初始化,因为不知道有多少元素。
//因此这种带有变量个数的数组,只能先声明,后赋值,不可直接初始化

数组的替代品(优于数组)

std::array

array<T,N>

include<array>

  1. if 创建array<>容器但不指定初始值,则数组也包含垃圾值
  2. fill()函数把所有元素设置为某个给定的值
array<double,100> values{};
values.fill(3.1415);// all elements to pi
  1. array.size(); 返回数组的元素个数size_t类型,array对象总是可以通过size()函数确定自己的大小
  2. array.at(i); 返回i出的值,i是一个索引
  3. array.front();返回数组第一个值
  4. array.back();返回数组最后一个值
  5. 两个array只要容器大小相同,存储类型相同,可以使用== != > < 对两个array中的元素进行逐个比对,也可相互赋值

std::vector<T>

  1. 大小可自动增加,可容纳任意数量元素
  2. 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};//初始化{}列表
  1. 与array类似,两个vector也可以比较,但是vector可以元素数量不同的比,字典序比较
  2. 一个vector给另外一个vector赋值时,被赋值的会被覆盖已经存在的值
  3. vector可以存储在其他容器中
  4. 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 指针和引用

指针

  1. 指针是可存储地址的变量。他指向内存中存储了其他值的位置
  2. 初始化列表为空,所以这个语句把pnumber初始化为等价于0的指针,即不指向任何内容的地址,等价于0的指针写为nullptr
long* pnumber{nullptr};
long *pnumber{nullptr};
  1. 定义指针时,总是要初始化他,如果还不能为指针提供期望的值,就将其初始化为nullptr
  2. 最好在单独的代码行上声明指针和普通变量,以避免出现这种混淆。
  3. 不管指针指向什么类型或大小的数据,指针变量本身的大小始终是相同的,如今的平台上指针变量大小一般是4或8字节

地址运算符&

  1. &是一个一元运算符,他可以获取变量的地址。
  2. &运算符可以应用于任何类型的变量,但必须在对应类型的指针中存储地址
  3. 使用编译器推断指针类型,但是尽量还是使用auto*
auto* number{&height};
  1. 使用auto*声明的变量只能用指针值初始化,使用其他类型的值初始化,会导致编译错误。

间接运算符

  1. 将间接运算符*应用于指针,可以访问指针所指向的内存位置的数据,也成为为解引用运算符
int count{};
int* pount{&count};
  1. *是乘法运算符,间接运算符,还可以应用于声明指针,一般编译器根据上下文分析
  2. 使用指针的用处
    1. 动态的为新变量分配内存空间,即可以在程序执行过程中分配
    2. 使用指针表示法操作存储在数组中的数据,与普通数组表示法完全等效
    3. 指针可以在函数中访问函数外部定义的大块数据
    4. 指针是支持多态性起作用的基础

char类型的指针

  1. 指向char类型的变量,可以用字符串字面量(实际上是const char类型的数组)初始化
  2. 给未解除引用的char类型指针应用<<插入运算符-----假定这种指针包含以空字符结尾的字符串的地址
  3. 如果给解除引用的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;//输出首字符(首地址)   解除引用
}
  1. 指针数组

每个因子都是一个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];
  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、

  1. 常量指针

存储在指针中的地址不能修改。

只能指向初始化时指定的地址。

但是地址的内容不是常量,可以修改,

int data{ 20 }; 
int* const pdata{ &data }; 
*pdata = 25;//内容可变
  1. 指向常量的常量指针

因为存储在指针中的地址和指针指向的内容都被声明为常量,所以两者都不能修改

const float value{3.14};
const float* const pvalue{&value};

指针和数组

  1. 数组名可以像指针那样操作,输出时,使用非char类型的数组名,就可以得到内存中的地址。
int num[]{ 1,2,3 };
cout << num << endl;
  1. 指针可进行的运算:

    1. 加减:(在数组中表现为向后/前移动一位,就是一个类型的字节数)

      • 指针+整数=指针

        int data[]{1,2,4};
        int* pdata{&data[1]};
        cout<<* (pdata+1)<<endl;//4
        
      • 指针-整数=指针

      • 指针+-指针=整数(类型相同,同一数组中)

      结果是两个索引的差值

    2. 比较

动态内存分配

  1. 所有指针都应该初始化,如果指针没有包含合法的地址,就应该总让他包含nullptr
double* pvalue{new double{}};//初始化为0.0
double* pnull{};//初始化为nullptr
delete pvalue;
pvalue = nullptr;
  1. delete 只是将自由存储区的内存释放,变量还可以使用,但是最好先将其置为nullptr

  2. 与普通数组不同的是,无法让编译器推断出动态分配数组的维数

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;
  1. 释放动态数组的内存,使用delete[] 或者delete [] 方括号表示删除的数组
  2. ->指针选择成员
  3. 多维动态数组?
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;
}
  1. 每个new必须对应一个delete,每个new[] 必须对应一个delete[]
  2. 在c++程序设计中,尽量不要使用new delete new[] delete[],应该使用vector<> 和智能指针来规避动态内存的一些风险。尽量避免直接操作动态内存。

智能指针

  1. 不必使用delete delete[] 运算符释放内存

  2. 智能指针不能进行递增或递减,也不能进行算术操作

  3. <memory>头文件

  4. 三种智能指针

    1. unique_ptr<T>

      1. 这个对象类似于指向T类型的指针,是惟一的。
      2. 从不会有两个unique_ptr<>对象指向同一地址
      3. 指向的值被该对象独占
      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) };//
      //三个方法相同,推荐最后一种写法,简洁,且可以防止内存泄漏
      
      1. reset() 将指针重置为nullptr,如果参数不设置值,就是nullptr

      2. 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;
        
  1. shared_ptr<T>

    1. 创建shared_ptr<T> 过程更复杂一些,主要是需要维护引用计数,

      void proj8()
      {
         shared_ptr<double> pdata{ new double{111.1} };
         shared_ptr<double> pdata2;//初始化为nullptr
         pdata2 = pdata;
         cout << *pdata2 << endl;
      }
      
      1. 复制pdata会增加引用次数,两个指针必须重置或释放,double变量占用的内存才会释放
      2. 实际使用共享指针的情况通常涉及对象
  2. weak_ptr<T>

理解引用

  1. 引用就是一个别名,可以用作某对象的别名,不可以只声明引用而不对其初始化
  2. 引用不能修改另一个另一个对象的别名
  3. 类型名后面&符号表示引用,如果取得引用的地址,结果会是指向原始变量的一个指针

就当做别名去理解,想不清楚就用原名

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 操作字符串

  1. c++的string头文件定义了string类型,相比ctring头文件中以‘\0’结尾的c字符串更可靠

  2. 定义string对象的六种方式

    1. empty
    2. 字面量
    3. 字面量切割
    4. 构造器
    5. string对象
    6. 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对象的切割

  1. c串与string串的转化,(string 字符串的长度不会计算\0,但是有\0存在(不理它))

    1. string.c_str();
    2. 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();

  1. string对象的操作

    1. 赋值
      • 字面量赋值
      • 串变量赋值
    string adj{"hornwogging"};//字面量赋值
    string word{"ribbish"};
    word{adj};//串变量赋值
    adj = "twotiming";
    
    1. 连接
    • +连接:必须有一个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(,'!');//!!!
    
    1. 数串连接:

    ​ to_string(数字)+字符串

    double num{ 100.0 };
    string name{ "I'm Huang Hongwei.I am " };
    cout << name + to_string(num) + " years old" << endl;
    
  2. 读入串getline(cin,text)

    string text{};
    getline(cin, text);
    cout << text << endl;
    
  3. 访问子串

    • 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;
      
  1. 比较字符串

    1. 字典序比较:

      • 前面的字符都相同,看长度,越长越大
      • 长度相同,且对应字符相同,则相同
    2. string对象可以存储到容器中,普通的char数组不能存储到容器中。std空间定义了一个非成员函数模板,实现效果与swap(a,b)相同

    3. 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;
            }
        }
        
    4. 使用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;
     }
    }
    

  2. 搜索字符串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开始搜索

  1. 修改字符串

    1. 插入 :索引位置前插入,充当当前索引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;
      
  1. 替换

    string text{ "we can replace a string" };
    text.replace(1, 5, "123456");//在index=1的地方,的5 个字符,替换为123456
    cout << text << endl;
    
  1. 删除

    删除[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 定义函数

返回类型 函数名 (参数列表)

{

}

  1. 函数调用中的实参顺序必须对应于函数列表里的参数顺序。函数名和参数列表的组合称为函数的签名。
  2. 函数体中可以有多个return语句,每个return语句可能返回不同的值。
  3. 返回类型是void->return ;
  4. 函数原型=函数声明:定义了函数名,函数的返回值,参数列表;
  5. 声明一定要放在调用之前。。除非把实现写在写在引用之前。(同c语言)

给函数传递实参

如果指定的函数实参类型不对应参数类型,编译器就会把参数的类型隐式转换为参数类型

按值传送:

实参的变量值或常量值根本不会传送给函数,而是创建实参的副本,把这些副本传给函数。执行完函数之后就废弃副本(不会对原有的值进行更改)

  1. 给函数传递指针:参数为指针类型时,按值传送机制就会像以前那样运行,但是指针包含另一个变量的地址,此时,指针的副本也包含这样一个副本
#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&

类型&

  1. 对于类类型这种,按值传送实参和包含因用的函数传参是没有区别的,但是按值传送会对值进行复制,造成内存的浪费

  2. 对比引用与指针

    1. 最明显的区别:传送指针的时候,先使用&获取一个值的地址,而在函数内需要使用*解引用
    2. 指针的鲜明特点:可以为nullptr,而引用必须为某个值。因此允许实参为null,就不能使用引用
    3. 使用引用可以写出更优雅的语法,但是if不看原型,分辨不清是否应用了引用,(和按值传送的时候长的一模一样)
    4. 一般认为const int& 比 const int *d更好用

    在使用指针之前必须测试指针是否为nullptr,而引用一般不需要担心

  3. 对比输入输出参数

    note: 一般不要使用 即作为输入参数,又作为输出参数的变量

  • 输入参数:一般两个选择:const引用传送:const string&(建议使用) 按值传送:string (会复制string对象)
  • 可以将T值传送给T&和constT&引用,但是只能将const T 值传送给const T&引用(当然其他也可以传送给他,比如T 表示别名)
  1. 按引用传送数组

    优点:不进行复制。能够修改原始值

    可以通过传递引用向函数传递精确的多维数组一维的个数

    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>

  1. string_view() 具有常量特性,他只需要指向某个实际对象:string 字符串字面量或其他任何字符数组中存储的字符序列,,初始化和复制string_view 的开销都特别低

  2. 隐式创建string会复制字符,但是创建string_view则不会

    string_view sv{"hjkl"};
    
  3. 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; 
}

从函数中的返回值

可以从函数中返回任意类型的值。主要讨论返回指针和引用中的陷阱

返回指针

在指针返回到调用函数的时,指针指向的变量必须仍然在其作用域中。

警告:不要从函数返回在栈上分配的自动局部变量的地址

  1. 不在调用程序中的地址
int* larger(int a, int b)
{
    //这些地址不在调用程序中,所以都是错误的
    if (a > b)
    {
        return &a;//wrong!
    }
    else
    {
        return &b;//wrong!
    }
}
  1. 在调用程序中的地址
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;
}

返回引用

  1. 不要从函数中返回自动局部变量的引用

  2. 想要返回对某个实参的非const引用,就不能把参数指定为const

  3. 参数不是const,所以不能把字符串字面量用作实参,编译器不允许这么做

  4. 在使用类创建自己的数据类型时,引用返回类型是必不可少的

返回类型的推断

  1. 类型推断可以节省时间:return 之后的数据类型要明确

  2. auto 不会推断为一个引用类型,而总是推断为一个值的类型,即使将一个引用赋值给auto,值也会被复制。而且值的这个副本不是const,除非使用const auto

  3. 要让编译器推断一个引用类型,可以使用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的参数唯一的区别就是,为引用定义参数还是为指针定义参数

  1. 对于基本类型 int和const int 是相同的

    //下面两个原型没有区别
    long larger(long a ,long a);
    long larger(const long a ,const long b);//编译器会忽略这个const
    
  2. 重载和const指针参数

    1. 在两个重载函数中,一个函数的参数是type *(会修改值),另一个函数的参数类型是const type *(一个只读指针,不会修改) 这两个函数是不同的

    2. 编译器不会把const值传送给非const指针参数的函数

      const 值会传送给const指针

      const int* larger(const int* a,const int* b);
      const int num{0};
      *larger(&num)
      
    3. 一个函数的参数类型是“指向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类型
      
    4. 重载和const引用参数

      不允许在&后面添加const

      T& (const常量无法赋值给T)和const T&(可以接受const和非const)是不同的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容