IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 2021-07-14:C++基础知识03 -> 正文阅读

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

C++基础知识03

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

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

Section-1.1:内存模型

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

现在,存放信息的“房子”我们明白了,那么如何去找寻房子呢?在硬件层面,我们可以用地址来找寻信息,任何变量、对象都有存储它的内存地址!至于是这块地址的左端还是右端,就不是硬件层面说了算了,是由编译器决定的!

譬如:

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

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

对于普通变量的地址是在声明的时候就确定了,随后的赋值,不管对象/变量如何变化,地址都是一样的!

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

示例程序如下:

#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

这是因为,如果不是引用,就不是左值!返回值如果是对象是不能作为左值的!只有左值才能继续链式操作!这一点,能大大提升编程的效率!

后记

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

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-15 15:59:28  更:2021-07-15 15:59:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/20 4:39:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码