2021-07-14:C++基础知识03

C++基础知识03

Section01:指向基本数据类型的指针

曾有人这么谈到计算机内存:“内存就像是一群连续的房子,每一个房子能够存储一些内容,每一个房子也有本身的门牌号,在必要的时候咱们能够经过门牌号去找到对应的房子访问须要的内容”。这个说法是否准确呢?咱们先来看看内存模型:ios

Section-1.1:内存模型

计算机中存储的最小的单位是bit,它的值只有0、1。可是这么小的位也不能表示不少信息,因而有了更普通的一种存储单位,就是字节byte,一般 1byte=8bit,能够表示无符号数[0,255];也能够表示有符号数[-128,127]。c++

如今,存放信息的“房子”咱们明白了,那么如何去找寻房子呢?在硬件层面,咱们能够用地址来找寻信息,任何变量、对象都有存储它的内存地址!至因而这块地址的左端仍是右端,就不是硬件层面说了算了,是由编译器决定的!程序员

譬如:面试

int a = 5;
double d = 3.14;
int* pa = &a;
double* pd = &d;

程序员很难记住地址(毕竟一个现代的地址有8位),为了简便,咱们用变量名来代替地址,能够认为是 变量名是地址的别名编程

对于普通变量的地址是在声明的时候就肯定了,随后的赋值,无论对象/变量如何变化,地址都是同样的!数组

而指针,其实就能够理解为一种地址!在初始化、赋值的时候,给普通变量赋予的是值,也就是对象值!可是,指针,在初始化和赋值的时候,直接赋予的是地址,表示它接下来的地址!ide

示例程序以下:函数

#include <stdio.h>

int main() {
    int a = 10;
    printf("%x\n", &a);
    a = 100;
    printf("%x\n", &a);
    int b = 5;
    printf("%x\n", &b);
    b = a;
    printf("%x\n", &b);
    int c = b;
    printf("%x\n", &c);
    printf("\n--------------pointer----------------\n");
    int* pa = &a;
    printf("%x\n", pa);
    pa = &b;
    printf("%x\n", pa);
    int* pb = pa;
    printf("%x\n", pb);
    return 0;
}

运行上述程序,会看到运行结果的表现是:学习

前三个普通基本数据类型变量a、b、c的地址不会改变!而指针在初始化后的地址是指向变量的地址!但当指针再次赋值后,则地址会发生改变,会与右值同时指向同一个地址!测试

好比个人输出结果:

61fe0c
61fe0c
61fe08
61fe08
61fe04
--------------pointer----------------
61fe0c
61fe08
61fe08

Section-1.2:指针与指针的访问

分析下面程序的内存模型:

#include <stdio.h>

int main() {
    int a = 112, b = -1;
    double c = 3.14;
    int* pa = &a;
    double* pc = &c;
    return 0;
}

模型图为:
在这里插入图片描述

因而能够获得两个结论:

结论1 :指针的声明与初始化方法

Type* ptr = &Var;

结论2 :指针与普通变量的赋值

任何变量的值,都是存储在分配的内存地址上的值,指针变量也不例外!

什么意思呢?咱们来看一下程序输出:

#include <stdio.h>

int main() {
    int a = 112, b = -1;
    double c = 3.14;
    int* pa = &a;
    double* pc = &c;
    printf("%d %d %d", &pa, pa, *pa);
    return 0;
}

它输出的结果是:

6422008 6422028 112

这说明了什么???

更具体的结论:

指针变量其实也有本身的地址,不能是指针变量就是地址,而是指针变量的值是地址!那么输出的内容112是什么呢?实际上是:指针变量所指向的地址的值!并非指针变量的值!指针变量的值是地址!!!

用更细致的图来描述:

在这里插入图片描述

pa指针变量的地址是6422008存储的变量是指向对象的地址6422028,而后所指向地址的值是a的值也就是*pa,是112。

Section-1.3:未初始化的指针与非法指针

典型错误1:未初始化的指针

指针若是没有初始化,那么该指针只有本身的地址,并无一个能容纳对象/变量/值的内存空间!例如错误范例程序:

int* pa;
*pa = 100;

虽然这种异常,在新标准的C/C++下修复了,可是仍然是一个潜在的问题,不能使用!

典型错误2:忽略了指针的值是地址

先看错误的范例程序,再看有啥问题:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int a = 112;
    int* pa = (int*)malloc(sizeof(int*));
    *pa = a;
    printf("%d %d\n", *pa, a);
    *pa = 200;
    printf("%d %d\n", *pa, a);
    return 0;
}

运行的结果是:

112 112
200 112

总结:必需要让指针指向某一个对象的地址,才能起到指针的做用,不然只是一个临时的值拷贝!

典型错误3:空指针的使用

不建议使用NULL,有的编译器源码规定NULL是0也有的不是,为了不指针与整型的差异,从C++11开始就用nullptr替代NULL了!而空指针、野指针也会带来许多危害,例如范例程序:

#include <iostream>
using namespace std;

int main() {
    int* ptr = new int(20);
    delete ptr;
    ptr = nullptr;
    return 0;
}

开始调试,会发现,在delete指针后,虽然对象被delete了,可是ptr仍是垂悬在内存的,因此须要置空,给你们可靠垂悬指针的样子:
在这里插入图片描述
再看看置空后的样子:

在这里插入图片描述

Section02:指针编程训练

训练1:实现字符串求长度函数strlen

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::cin;

size_t strlen(const char* pstr) {
    size_t length = 0;
    while (*pstr++)
        length++;
    return length;
}

int main() {
    std::string s;
    cin >> s;
    cout << strlen(s.data()) << endl;
    cout << s[0] << endl;
    return 0;
}

测试的输入是:

Hello

输出是:

5
H

总结本题:

这里呢,strlen函数的传参,传参方式实际上是值传递,由于传的自己就是值而不是指针!总的来讲,要实现指针传递,就必须传当前实参的地址!而当前实参若是是指针就必须传指针的地址也就是说,须要形参用指针的指针来接收指针的地址!

训练2:题目描述以下

在这里插入图片描述

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::cin;

char* find_char(char const* source, char const* chars) {
    const int MAX_SIZE = 128;
    char* pRes = new char[MAX_SIZE]();
    char const* pChars = chars;
    char* res = pRes;
    while (*source) {
        while (*pChars) {
            if (*pChars == *source) {
                *pRes++ = *source;
                break;
            }
            ++pChars;
        }
        pChars = chars;
        ++source;
    }
    *pRes = '\0';
    return res;
}

int main() {
    const char* src = "Hello World, my baby!";
    const char* chs = "emb";
    cout << find_char(src, chs) << endl;
    return 0;
}

按照个人测试用例,输出结果是:

embb

训练3:题目描述以下

在这里插入图片描述

#include <iostream>

using std::endl;
using std::cout;

void reverse_string(char* string) {
    char* pEnd = string;
    char* pStr = string;
    while (*(pEnd + 1) != '\0')
        ++pEnd;
    while (pStr < pEnd) {
        char cTemp = *pEnd;
        *pEnd = *pStr;
        *pStr = cTemp;
        ++pStr, --pEnd;
    }
}

int main() {
    char src[] = "Hello World";
    reverse_string(src);
    cout << src << endl;
    return 0;
}

输出的结果是:

dlroW olleH

总结《训练3》

在本题中,要强调和学习字符指针和字符数组的区别!!!

区别1:字符指针不可写

字符数组是知道大小的,因此你能够对其中某个元素进行写操做!而字符指针只是指向一个字符串,并不知道其具体大小,因而不可写!也就是不能访问!这就意味着,若是本体你传入的不是字符数组src[],而是一个指针,那么就会段错误!由于不可写!

区别2:字符指针有常量池,字符数组是独立的空间

看下面这个示例:

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::cin;
 
int main () {
    char* ps1 = (char*)"Hello";
    char* ps2 = (char*)"Hello";
    cout << (ps1 == ps2) << endl;

    char arr1[] = "Hello";
    char arr2[] = "Hello";
    cout << (arr1 == arr2) << endl;
    return 0;
}

输出结果是:

1
0

这是由于,C/C++中也有字符串常量池,和Java同样!若是是指针,都会指向常量池的同一个地址!而若是是数组呢?就不会了!由于每个数组都是独立分配的空间!

训练4:本身实现一个strcpy(面试题)

#include <iostream>

using std::endl;
using std::cout;

void strcpy(char* dest, const char* src) {
    while ('\0' != *dest)
        dest++;
    while ('\0' != *src)
        *dest++ = *src++;
    *dest = '\0';
}

int main() {
    char dest[] = "Hello";
    char src[] = " World";
    strcpy(dest, src);
    cout << dest << endl;
    return 0;
}

输出的结果是:

Hello World

Section03:引用初步

什么是引用呢?其实引用的概念比起指针来讲好理解的多!

引用相对于给对象/变量起了一个别名,程序员不管是访问引用仍是访问对象/变量,都是起到了同样的效果!不管是改变了引用仍是改变了对象/变量,另外一方也会随之改变!引用的定义格式是:

Type& ref = Var;

要注意,容许没有初始化的指针,可是绝对不容许没有初始化的引用!为何呢?你想一想,指针其实本身也是一个变量,它有本身的地址有本身的空间,而指针呢?一旦没有肯定是谁的别名,它就不可能存在!就像一我的的外号确定是和这我的自己息息相关的,本名都没有,还谈什么别名呢???

另外,指针随时能够指向其余的对象/变量,而引用不能够,例如:

示例:指针能够随时指向别的变量/对象

#include <iostream>

using std::endl;
using std::cout;

int main() {
    int i1 = 10, i2 = 20;
    int* p1 = &i1;
    *p1 = 100;
    cout << i1 << endl;
    p1 = &i2;
    i2 = 200;
    cout << *p1 << endl;
    return 0;
}

输出结果是:

100
200

这是由于,在执行p1=&i2;后,执行了i2的地址!而引用,则是从一而终的!

示例:引用的从一而终

#include <iostream>

using std::endl;
using std::cout;

int main() {
    int i1 = 10, i2 = 20;
    int& r1 = i1;
    r1 = 100;
    cout << i1 << endl;
    r1 = i2;
    cout << i1 << endl;
    r1 = 200;
    cout << i1 << endl;
    return 0;
}

输出的结果是:

100
20
200

这是由于执行语句r1=i2;引用没有起到真正的做用!引用r1仍是i1的别名!那第二个20是如何出现的呢?其实,虽然引用从一而终,可是当执行r1=i2的时候,会将i2的值赋给r1,而不是让r1指向i2!

总结重点:

  1. 引用必须是引用变量,不能是常量或表达式!
  2. 能够有常量的引用,可是只能保证不能经过常引用本身来改变被引用的对象/变量!
  3. 定义引用的同时要当即初始化!
  4. 引用是从一而终的,及时执行的赋值操做,也只是赋值,而不是改变引用的对象!

Section04:引用传参与引用返回值

Subsection-4.1:引用传参

在传参的时候,我建议,能用引用的就尽可能都用引用!这是由于,不用引用会形成拷贝!哪怕你是指针都会拷贝的!若是是单一的变量、对象都还好,若是是容器呢?那岂不是会形成巨大的拷贝,并且是每次调用都会浪费这个拷贝的资源!

进一步说,若是能用常引用那就是最好的,常引用有更强的功能,不光是能实现常量,还能包容左值引用和右值引用!

这个还得看一些OJ题,我记得之前我作PTA的题,就遇到一个,排序的时候若是参数不用引用,因为拷贝过于频繁,会超时!若是是引用,就能AC!

Subsection-4.2:引用返回值

这个引用的返回值,功能就大了!先举一个例子:

#include <iostream>

using std::endl;
using std::cout;

int n;

int& getValue() {
    return n;
}

int main() {
    getValue() = 100;
    cout << n << endl;
    return 0;
}

输出的内容是:

100

怎么样?这个功能是否是有点奇妙了???

总结:引用做为返回值的时候,该函数能够做为对象左值!

再例如,咱们经过一个成员函数给对象赋值:

#include <iostream>

using std::endl;
using std::cout;

class Integer {
private:
    int v;
public:
    Integer() {}

    Integer(int v_) : v(v_) {}

    Integer& getObject() {
        return *this;
    }

    int getValue() {
        return v;
    }
};

int main() {
    Integer i;
    i.getObject() = 20;
    cout << i.getValue() << endl;
    return 0;
}

输出的结果是:

20

总结:返回引用的编程技法

使用条件:

必须是,在调用函数前,返回的对象就已经完成了声明!也就是说,返回对象的引用,该对象的做用域必须包含该函数的调用段。

能实现的效果:

  1. 可以做为左值,改变对象,至关于直接对对象赋值!那是由于这是引用,对引用的操做能实际的落实到被引用的对象上!
  2. 可以做为左值,实现链式反应!这一点很关键,可让程序简化!

Subsection-4.3:返回引用带来的链式反应

示例:重载输出运算符的链式反应
#include <iostream>
#include <string>

using std::endl;
using std::cout;

class Person {
    friend std::ostream& operator<<(std::ostream& os, const Person& rhs);
private:
    std::string name;
    int age;
public:
    Person(std::string name_, int age_) : name(name_), age(age_) {

    }
};

std::ostream& operator<<(std::ostream& os, const Person& rhs) {
    os << rhs.name << "\t" << rhs.age;
    return os;
}

int main() {
    Person p1("Name1", 21);
    Person p2("Name2", 22);
    cout << p1 << endl << p2 << endl;
    return 0;
}

输出结果是:

Name1 21
Name2 22

若是重载输出流运算符的函数,不返回引用,大家猜一猜会发生什么事情???

其实很简单,若是不返回引用,则在执行:

cout << p1 << endl << p2 << endl;

的时候,获得输出流式对象p1后,流对象就销毁了,由于不是引用做用域不能一直从一而终的!为了能链式地,输出完p1后接着输出p2,就必须用返回引用的方式!

示例:深刻理解左值在setter的用法

仍是Person类的例子,看看下面的程序:

#include <iostream>
#include <string>

using std::endl;
using std::cout;

class Person {
    friend std::ostream& operator<<(std::ostream& os, const Person& rhs);
private:
    std::string name;
    int age;
public:
    Person& setName(std::string name) {
        this->name = name;
        return *this;
    }

    Person& setAge(int age) {
        this->age = age;
        return *this;
    }
};

std::ostream& operator<<(std::ostream& os, const Person& rhs) {
    os << rhs.name << "\t" << rhs.age;
    return os;
}

int main() {
    Person p1, p2;
    p1.setName("Name1").setAge(18);
    p2.setName("Name2").setAge(21);
    cout << p1 << endl << p2 << endl;
    return 0;
}

输出的结果是:

Name1 18
Name2 21

这是由于,若是不是引用,就不是左值!返回值若是是对象是不能做为左值的!只有左值才能继续链式操做!这一点,能大大提高编程的效率!

后记

今天详细阐述了,指针、引用的关系和用法,而且针对一些问题设计和展现了许多编程训练题和编程案例!欢迎一块儿交流讨论!

相关文章
相关标签/搜索