分类: 技术学习

  • 继承和多态 | Java语言关键

    继承和多态 | Java语言关键

    前一篇封装的文章内说到,面向对象程序三大特性:封装、继承和多态。那么接下来讲下面两个特性:继承、多态。

    继承

    继承这里的概念,就是字面意思,核心就是要搞清楚“父与子”

    为什么会用到继承?试想一下,我们现在要做一个教务管理系统,那么其中的用户角色有学生、老师、管理员,我们肯定是对这些角色分别建一个类,不过每个类中有诸如头像、姓名、年龄等这些每个都相同的成员变量,那么我们真的要这三个类内每个人都写一遍这些吗?其实不然,我们想到这些人都是用户,那么我们可以做一个用户的父类,让这些角色的类进行继承。所以,用继承的作用是通过共性抽取去提高代码的复用和方便后续维护。比如,如果我发现每个人都漏了一个成员,那么我可以加到这些子类关联的父类里;如果我要给学生的类加特有的学号和班级的信息,那么我直接在学生子类内加即可。

    不过继承还有个作用就是实现多态,将在下文讲到。

    extends 关键字

    用于表示两个类间的继承关系是 extends 关键字,上述说明的代码如下(适当简化)。

    // User.java文件
    public class User {
        public String name;
        public int age;
    
    }
    
    // Teacher.java文件
    public class Teacher extends User {
        public int teacherId;
        public String teachClass;
    }
    
    // Student.java文件
    public class Student extends User {
        public int studentId;
        public String class;
    }

    extends 关键字前面是子类(也叫派生类),后面是父类(也叫基类、超类)。同样的,成员方法也可以从父类继承到每个子类。

    但是注意的是,Java不支持多父对一子的继承,即用代码表示如 public class Teacher extends User, TestUser

    同时也不推荐出现超过三层的继承关系,不要有太复杂的继承关系。

    子类访问父类

    当子类继承了父类后,可以访问父类中的成员变量和成员方法等。如果是子类成员变量和父类成员变量不同名的情况下,是可以直接访问的。如果是出现同名的情况下,优先访问子类的成员变量。但是如果子类中访问了父类不存在成员变量,显然会编译报错。由此,子类对成员变量的访问可以认为是就近原则

    对于成员方法,也是如此。不过之前在类和对象中讲过,方法是能重载的(即方法名相同,参数列表和返回值不同的情况),那么如果父类和子类间的相同方法名的时候,可以按需要选择适合的方法访问。

    成员方法中还有构造方法,要注意的是,应该是先调用父类的构造方法,再调用子类的构造方法。(简单去记,那对于现实中同样是这样“先有父才有其子”的关系)

    既然基本上是子类成员优先,但是我如果即希望保留子类的成员,又想访问父类的成员呢?

    super 关键字

    super 可以认为是子类内从父类继承来的这块内存的内存的标识。那么就需要用到 super.成员 来在子类去访问父类的成员了。注意只能在子类的非静态成员方法中去访问父类的成员。

    上面对于构造方法的说明中,我们明白需要先在子类对象构造完成之前,帮助父类对其中的成员进行构造。那么此时调用父类的构造方法就需要super() 。如果父类的构造方法是含有参数的,super() 括号内也只管填入参数列表即可。这个语句只能放在子类的构造方法的第一行,且不能和 this 同时出现。

    // User.java文件
    public class User {
        public String name;
        public int age;
    
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
    }
    // Student.java文件
    public class Student extends User {
        public int studentId;
        public String gender;
    
        public Student(String name, int age, int studentId, String gender){
            super(name,age);
            this.studentId=studentId;
            this.gender=gender;
        }
    }

    真的需要每次我们都要在子类的构造方法内第一行写 super() 嘛?其实如果在父类只有一个不带参数的构造方法的情况下,会自动在子类的构造方法第一行前有隐含的 super(); 调用。

    我们会发现这个表达和我们之前文章中讲的this引用时很相似,我们对比下:

    表达方式 / 作用superthis
    关键字.成员子类中访问父类的成员访问当前类对象的成员
    关键字()子类中访问父类的无参构造方法访问当前类对象的无参构造方法

    同样,也有很多共同点:比如第二种表达方式都只能放在方法内的第一行,只能用来访问非静态成员方法和字段。

    代码块执行顺序

    我们知道静态是依赖于类的,所以静态代码块 static {} 是在类加载时执行,即最先被执行的,在 父类和子类的关系上也是如此,父类的静态代码块先于子类的代码块的执行。

    还有种代码块叫实例代码块,是直接用 {} 括起来的(也不带访问修饰限定符)。比如我们无法在类中直接放赋初值语句等,但是可以放到实例代码块内。实例代码块的执行优先级是高于构造方法的执行,原因是给对象分配内存的时候,接下来就是实例化的过程:实例变量→实例代码块→构造方法,而且重要的是实例代码块可能依赖于对象实例变量的状态,但不一定需要构造方法提供的具体参数。

    对于继承关系的两个父子类,那么父类的实例代码块执行,再父类构造方法执行完后才能去执行子类的实例代码块,再执行子类的构造方法。上文我们提到还有静态代码块,如果父类和子类均有静态代码块的话,则顺序依次是:父类静态代码块→子类静态代码块→父类实例代码块→父类构造方法→子类实例代码块→子类构造方法。(顺序简析:父子类被加载,静态代码块先父后子执行,父子类对应的对象被创建后需要实例化,先父后子完成实例化过程)

    protected 访问修饰限定符

    范围private默认protectedpublic
    同个包同个类
    同个包不同类
    不同包内子类
    不同包非子类

    经过本文说明,到这儿就能明白这里的子类是什么意思了。如果被 protected 修饰了成员变量,成员方法等表示要么只能在同一个包中的类中进行访问,要么在不同包中只能通过在继承关系上的子类对象来访问。

    形象地说,protected 就可以看作是一个被保护的“家”,这个房间是家里的人“父与子”还有有关系的亲戚都可以使用的,外部的人如果没有与家中的人有亲属关系是不能访问的。

    结合上一篇文章,我们基本讲完了四大访问修饰限定符,什么时候该用哪种呢?

    我们希望类要尽量做到“封装”,只暴露出必要的信息给类的调用者,而无需关心其内部实现。所以在设计类的时候应该尽可能的使用比较严格的访问权限

    final 限制继承

    final 关键字去放在成员变量的访问修饰限定符前,代表这个变量值已经完全确定,此时表示常量。

    final 关键字去放在类的访问修饰限定符前,代表这个类无法继承了。如我们常用的 String 类。

    组合

    组合也是像继承一样可以代码复用的一种方式。组合是指在新类中创建现有类的对象。

    // 假如这是一个APP的组成
    
    // 导航条
    class NavBar{
    
    }
    
    // 顶部banner
    class Banner{
    
    }
    
    // 内容元素
    class Content{
    
    }
    
    public class App {
        // 这部分就是实现了组合
        private NavBar nav;
        private Banner banner;
        private Content content;
        
    }

    需要区分的是,组合表示出来的是一种「has-a」的关系,而继承则是「is-a」。组合是类A 拥有 类B 的一个实例,A类包含了B类的实例作为其成员变量,来实现“整体”与“部分”之间的关系;继承是子类 父类的一种特殊类型,比如上文中Student是User中的一种特殊用户。

    而且一般推荐是:能用组合的尽量用组合。因为使用继承,不可避免地打破了封装性(违反OOP原则),迫使开发者去了解父类内的细节,而且父类的更新可能导致bug,对程序维护来说并不有利。

    多态

    多态也是字面意思, 不同的对象会有不同的状态。比如同样是叫声,猫叫和狗吠肯定不一样。

    要实现多态有三个必要条件,缺一不可:在继承体系下、子类必须要对父类中的方法进行重写、通过父类的引用调用重写的方法。所以多态的变化是根据方法里传递不同类的对象的时候来体现的。

    方法的重写

    介绍

    之前的文章中有提到过方法的重载,是指同个方法名但是不同的参数列表、返回类型和访问修饰符,这使得同一个方法名可以根据参数的不同实现不同的功能。

    那么,方法的重写是发生在子父类间的同名、非static、非private修饰、非final修饰的方法中,当子类重新定义父类中的方法时,会覆盖父类的方法实现。

    还需要注意:返回值、形参、方法名都是要和父类中的一致(外壳不变,核心重写);子类重写方法的访问权限不能⽐⽗类中被重写的⽅法的访问权限更低;在父类被staticprivate修饰的方法、构造方法都不能被重写。

    在IDEA中,对于重写的方法可以使用 @Override 注解来显示指定,而且IDEA可以识别到这个注解去进行合法性校验。

    综上,其实方法重载是一个类的多态性的体现,而方法重写是子类与父类的一种多态性表现。

    避免在构造方法中调用重写方法

    先说结论:用尽量简单的方式使对象进入可工作状态。尽量不要在构造方法内调用方法。

    如果出现了构造方法内要调用的方法被子类重写了以后,我们走一遍流程就会发现bug:首先,在构造子类对象的时候,要优先构造父类对象由此就会调用父类的构造方法。父类的构造方法内会调用被子类重写的方法,但由于子类对象还未完全初始化,此时调用该方法可能导致异常或未定义行为。

    向上转型

    父类引用指向子类对象,就是表达为 User stu = new Student("nibbles",18,123456,"男"); stuUser 类型的引用变量其指向了其子类 Student 类型的对象。

    这样进行向上转型了虽然无法使用子类的特有的方法,但是可以在方法传参的场景下可以把形参作为父类型引用去接收任意类型的子类对象,也可以作为方法返回值来返回子类对象(如 return new Student(...))。

    如果说有两个父类引用都指向了子类对象,都对父类里的一个方法进行了重写。那么调用该方法时,实际执行的是子类中重写后的方法,这就是动态绑定机制,也是多态性的体现。

    向下转型

    在上述向上转型后,如果还想调用子类特定的方法,此时就需要把父类引用再还原回子类对象。

    package cn.nibbles.animaldemo;
    
    // Animal.java
    public class Animal {
        public String name;
        public int age;
    
        public void eat(){
            System.out.println("eating");
        }
    }
    
    // Cat.java
    public class Cat extends Animal{
        public Cat(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void mew(){
            System.out.println("cat mewing");
        }
    }
    
    // Dog.java
    public class Dog extends Animal{
        public Dog(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void bark(){
            System.out.println("dog barking");
        }
    }
    
    
    // Test.java
    public class Test {
        public static void main(String[] args) {
            Cat cat = new Cat("a cat" , 1);
            Dog dog = new Dog("a dog" , 2);
    
            Animal ani = cat;
            ani = dog;
            // 模拟父类引用变量多次更换对象
    
    //        cat = (Cat)ani;
    //        cat.mew();
            // ClassCastException异常
    
            dog = (Dog)ani;
            dog.bark();
            // 正确向下转型到原子类对象
        }
    }
    

    但是,这里就出现个问题,如果我忘记了之前父类型的引用变量对应的子类型的话,那程序就会出现ClassCastException异常,可以使用 instanceof 运算符在类型转换前进行检查,避免此类异常的发生。

    那么上面的示例代码中向下转型的部分可以改写为:

    // Test.java
    package cn.nibbles.animaldemo;
    
    public class Test {
        public static void main(String[] args) {
            Cat cat = new Cat("a cat" , 1);
            Dog dog = new Dog("a dog" , 2);
    
            Animal ani = cat;
            ani = dog;
            // 模拟父类引用变量多次更换对象
    
            if (ani instanceof Cat){
                cat = (Cat)ani;
                cat.mew();
            }
            // ClassCastException异常 -> 检查不通过、不报异常
    
            if(ani instanceof  Dog){
                dog = (Dog)ani;
                dog.bark();
            }
            // 正确向下转型到原子类对象 -> 正常执行
        }
    }
    

    圈复杂度

    核心思想是用代码中的控制流图环路数量(即独立路径数)来衡量一个程序的复杂度。圈复杂度反映了代码中“决策点”的数量,也就是需要做判断的地方(如 ifwhilefor等)。值越高,说明代码的逻辑分支越多,测试起来更复杂,也更难维护。

    而多态就是可以减少圈复杂度,不必写那么多分支结构。可扩展能力也更强,能直接新增一个新的类去继承父类即可。

  • 包和封装 | Java语言关键

    包和封装 | Java语言关键

    包和封装也是类这部分的知识,类和对象请见-> https://nibbles.cn/java-class-obj.html

    包(Package)是组织类的一种方式,可以防止类名冲突和提高代码的可读性。可以简单看成里面都是装类的文件夹。包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式。

    导入

    包的导入可以使用 import 语句导入,比如导入util包下的Date类可以如下:

    import java.util.Date;
    
    public class Main {
        public static void main(String[] args) {
            Date day = new Date();
        }
    }

    一般来说,我们直接输入类名后,如果是自带在JDK内的,由IDEA会自动帮我们添加。

    如果你也尝试了上面这样在输入 Date 后,会发现出现了两个同名类:

    那么,这就是包的作用,把同名的类放在不同的包中做好区分。而且包名一般都是小写的。如果是java. 开头则代表是Java标准库的包命名空间前缀,util代表是子包名称。

    所以为了防止冲突,最好不要使用 import java.util.*; 这样通配符的形式。或者在创建对象的时候从包到具体类写全:java.util.Date date = new java.util.Date();

    还有一种方式是使用 import static 导入包中的静态的方法和字段,比如下面的二元一次方法求根两种写法:

    public class Main {
        public static void main(String[] args) {
            double a = 1.0;
            double b = 2.0;
            double c = 1.0;
    
            double ret1 = (-b+Math.sqrt(b*b-4*a*c))/(2.0*a);
            double ret2 = (-b-Math.sqrt(b*b-4*a*c))/(2.0*a);
    
            System.out.println(ret1);
            System.out.println(ret2);
        }
    }
    import static java.lang.Math.*;
    
    public class Main {
        public static void main(String[] args) {
            double a = 1.0;
            double b = 2.0;
            double c = 1.0;
    
            double ret1 = (-b+sqrt(b*b-4*a*c))/(2.0*a);
            double ret2 = (-b-sqrt(b*b-4*a*c))/(2.0*a);
    
            System.out.println(ret1);
            System.out.println(ret2);
        }
    }

    自定义包

    右键src文件夹创建的时候选择Package即可。一般的包名会使用域名的颠倒形式(如 cn.nibbles.mypackagename )包名要和文件路径匹配(如创建的cn.nibbles.mypackage 包的文件夹目录为cn/nibbles/mypackage)。

    包内的类文件顶部都会通过package关键字指定路径:

    package cn.nibbles.mypackage;
    
    public class NiBu {
        public String name;
        private int age;
    
        public nibu(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public int getAge() {
            return age;
        }
    }
    

    访问权限控制

    在定义类的时候,如果没有指定成员变量/方法的访问权限,则默认(什么都不写)就是包访问权限。同样是权限的关键字还有:privatepublicprotected 访问权限的用途就是控制方法或者字段能否直接在类外使用。

    如上我们已经创建好的 NiBu 类,如果在其他地方访问,那么成功导入类和创建对象后,只有name是可以被访问到的,age是只能在类内使用,如果不写则是不允许被其他包内的类访问。

    范围private默认protectedpublic
    同个包同个类
    同个包不同类
    不同包内子类
    不同包非子类

    protected 涉及到继承,在后续会说到

    封装

    面向对象程序三大特性:封装、继承、多态。接下来就是对封装概念引入。

    封装的东西大家在平时生活中也总是用到。比如对于芯片,我们直接看一般就是个黑色的片状还带有引脚,主要通过引脚与电路交互,我们只需要知道各引脚的功能即可。对于一台装好的主机,我们直接看就是个机箱还带有一些接口,我们只用插好对应的外设和插头即可使用,无需关注内部元件之间的线路走向等。

    封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

    适当的封装,可以将对象使用接口的程序实现部分隐藏起来,不让用户看到,同时确保用户无法任意更改对象内部的重要资料,若想接触资料只能通过公开接入方法(Publicly accessible methods)的方式( 如:”getters” 和”setters”)。
    ——封装 (面向对象编程)(Wikipedia

    那么,其实看完这里的说明,你或许发现了,我们从前一篇文章的Getter和Setter,到现在private访问权限的讲解,都是为了封装所服务的。

    不直接通过引用来访问其中的数据,而使用公开的作为接口的方法来进行访问。隐藏并保护了对象的内部实现细节,保护数据不被意外修改或破坏。这些都是封装的作用。

    static 关键字

    Java中被static修饰的成员成为静态成员,那么成员变量和成员方法都有静态和非静态的两种。而静态成员也被称为类成员,其不属于某个具体的对象,是所有对象所共享的。静态依赖于类,非静态依赖于对象。

    对于“共享”的理解,比如我只需要做这一个学校的记录学生信息的学生类,大家都是一个学校所以类内的代表学生所在学校的成员变量就可以是静态成员变量,所有的对象都是一个学校的值。

    package cn.nibbles.mypackage;
    
    public class StudentInfo {
    
        public static String school;
        public String name;
        public int age;
        public String className;
    
    }
    

    静态成员变量

    如上,school 就是一个静态成员变量(也成为类变量),有几点特性:首先就是,类的属性,所有对象共享的;既可通过对象访问还可以通过类名访问(更推荐后者);类变量存储在内存的方法区;类变量的生命周期同类(随着类的加载而创建,随类的卸载而销毁)

    静态成员变量的访问可以通过 “类名.静态成员变量名”,如上就是通过 StudentInfo.school 访问。当然访问还是要遵循访问控制的关键字划分的权限。

    静态成员变量的初始化,除了直接的就地方式 public static String school="Nameless" ,还有种方式就是静态代码块初始化。代码块的概念很简单,就是用 { } 扩起来的。静态代码块形如

    package cn.nibbles.mypackage;
    
    public class StudentInfo {
    
        public static String school;
        public String name;
        public int age;
        public String className;
        
        static {
            school = "Nameless";
        }
    
    }
    

    但是静态成员变量一般不会放在构造方法中来初始化,构造方法来初始化的只是跟对象有关的。但如果非要这么做,则需要静态的Setter。

    静态成员方法

    同样的,被static修饰的成员方法称为静态成员方法,是类的方法。所以也有几个特性:推荐使用“类名.静态方法名()”方式调用,如 StudentInfo.getSchool() 这样;不能在静态方法中访问任何的非静态成员变量,也不能在静态方法中调用任何非静态方法,原因是非静态是依赖于对象,就像是非静态方法内有this参数是无法知道引用的对象的。

  • 类和对象 |  Java语言关键

    类和对象 | Java语言关键

    在之前文章中已经入门简单的语法,那么接下来将讲到Java中很重要的概念——面向对象。

    在Java的Wikipedia中多范式(Paradigm)的条目中是这样解释的:

    genericobject-oriented (class-based)functionalimperativereflectiveconcurrent
    通用、面向对象(基于类)、函数式、命令式、反射式、并发

    介绍

    是一种创建对象的图纸,对象是根据所创建的实例。

    可以在类中定义好属性(描述其的变量)和方法(可以执行的操作)。而根据类来创建的对象,也就具有了这些属性和可使用的方法。如果要创建多个同类对象,只需要根据同一类来创建这多个对象即可,这样大大方便了代码的复用与维护。

    面向对象(OOP)可以看作为是一种编程思想,指的是利用对象之间的交互去实现功能。

    这样以面向对象的形式来组织代码,就可以做到比如引入外部的类,直接使用方法和属性,而且无需知道和细究方法内部的具体实现,只需要知道方法能做到的功能即可。

    对一个对象的重新认识的一个过程就是对对象进行抽象。

    类的定义

    public class Demo {
        public int number;
        public void out(){
            System.out.println("OUT");
        }
    
    }

    如上,一个类的定义包含:

    • 类的名字,一般我们使用大驼峰命名法(如ClassName),名字中每个英文都是大写。
    • 成员变量
    • 成员方法

    一般建议,一个文件中定义一个类。 且public修饰的类名必须要和文件名相同,而且这样的类名最好通过IDE的内置工具修改。

    你可能会感觉到这和C语言的结构体很像,但是与之不同的是,Java的类是面向对象的,且包含了成员变量和成员方法,支持访问控制修饰符(public/private等),并且还有继承、多态等多种机制。这些会在后续展开。

    类的实例化=对象

    用类类型创建对象的过程,称为类的实例化。实例化的代码:

    public class Test {
        public static void main(String[] args) {
            Demo obj = new Demo();
            obj.out();
        }
    }

    这样如第三行所示的格式,就是一个创建对象的语句。(关键字是new , 所以常有人说“new一个对象”)

    而创建好的名为obj的对象就是根据名为Demo的类创建的。之后我访问了Demo类中的成员方法(见第四行),访问或修改类中的成员一般使用点号( . ),这样就成功输出了“OUT”

    这样从内存空间存储的角度上,类这个“图纸”其实并不会占用程序运行时候用的堆内存,只是作为一种设计蓝图,一种元数据被存储在方法区,而根据类创建出对象才是真正会开辟堆内存空间的,所以这样也就更好地理解了为什么创建对象能被称为“实例化”。

    this引用

    在成员方法内,当出现方法中定义的形参和成员变量相同的时候,就需要用this+点号( this. )来表示指明是这个对象的成员变量。

    类似的,前面说到可以一个类可以创建多个对象,那么试想在这个类中有负责传入参数的成员方法,程序如何知道传给哪个对象相应的数据?同样也是借助了 this 对当前对象的引用。

    回到前面,讲讲如何对这个对象内的成员变量进行赋值。

    如果只是类内对成员变量指定初始值的话直接就是一条 public int yes = 1;即可,但是不可以拆开来两条去写,如public int yes; yes = 1; 。这是因为对于类内只能有“成员的声明”和“初始化块”,而前条语句会被判为初始化,而后者会被认为是普通语句不可使用。

    如果对于对象,不同的对象希望有不同的变量值的话,则需要写修改器,这也是种成员方法。而且对于IDEA很简单,可以直接借助内置工具生成。在类的文件内右键选择 Generate… (ALT+Insert快捷键) -> 选择 Setter, 然后在弹出的窗口中选中需要使用的修改器传入值的变量。则编辑器会直接生成如下的的代码:

    public void setNumber(int number) {
        this.number = number;
    }

    你会发现,在第二行好像自带了一个 this ,没错,就是在这里做到了让程序知道值应该传递给哪个对象。只要哪个对象调用了这个方法,那么这个 this 就是代指哪个对象。只能引用当前对象,不能再引用其他对象。而且有的时候编译器会在成员方法会被执行的时候将其自动传递。

    看一个自动传递的例子,还有种方法叫做访问器,这也是种成员方法。还是右键,Generate… (ALT+Insert快捷键) -> 选择 Getter,然后在弹出的窗口选择需要输出的变量。就会生成一个我们很熟悉的用于打印变量的方法:

    public void numOut(){
        System.out.println(number);
    }
    // 一般直接生成的是上面这样的
    
    public void numOut(){
        System.out.println(this.number);
    }

    其实简单改改自动生成的方法如第二段所示的,会发现结果也是一样。而这种情况即使不写,编译器也会自动加上 this

    构造方法

    也成为构造器,也是一个特殊的成员方法。名字必须与类名相同,在创建对象的时候由编译器自动调用,并且在整个对象的生命周期内只使用一次。

    // 文件Student.java中
    public class Student {
        public String name;
        public int age;
        public String gender;
    
        // 构造方法
        public Student(String name, int age, String gender) {
            this.name=name;
            this.age=age;
            this.gender=gender;
        }
        public Student(){
            this.name="nameless";
            this.age=0;
            this.gender="boy";
        }
    
    }

    需要注意的几点是:无需设置返回值也不能设置,可以重载(即同一方法可以按照使用场景设置不同的参数,如上无参数的构造方法,可以作为指定成员变量的默认值的一种方式)

    如果开发者没有显式定义(如上直接在类中写出构造方法即为显式定义),则编译器会默认生成一个不带参数的构造方法。如果有则不再额外生成。

    如果只是无参来指定默认值的话,上述无参的构造方法还可以用this简化为:

    public class Student {
        public String name;
        public int age;
        public String gender;
    
        // 构造方法
        public Student(){
            this("nameless",0,"boy");
        }
        public Student(String name, int age, String gender) {
            this.name=name;
            this.age=age;
            this.gender=gender;
        }
        
    }

    注意,使用this()调用其他构造方法时,必须将该调用语句放在构造方法的第一行

    绝大多数情况下使用public来修饰,特殊场景下会被private修饰(单例)

    对象打印

    public class Main {
        public static void main(String[] args) {
            Student stu = new Student();
            System.out.println(stu);
        }
    }

    用上面的代码直接打印会输出 Student@3b07d329 这样的“类名@hash值”的格式。

    但是如果想要打印对象中的成员变量,可以使用类中重写的 toString 方法。因为在上面的例子中,对于对象是直接进行了“sout”打印,实际上在类中是在调用一个默认的 toString 成员方法。

    IDEA也可以生成一个简单的重写方法,右键,Generate… (ALT+Insert快捷键) -> 选择 toString -> 选择要打印的成员变量即可。

    public class Student {
        public String name;
        public int age;
        public String gender;
    
        public Student(){
            this("nameless",0,"boy");
        }
        
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender='" + gender + '\'' +
                    '}';
        }
    }

    如果希望自己写 toString ,记得加上 @Override 注解确保正确重写。

    之后会继续讲包、封装、还有继承、多态等,虽然在本文有部分是提到但暂不细讲,循序渐进。

  • 运算符 | Java语言基础

    运算符 | Java语言基础

    运算符 不仅是 Java ,在众多编程语言中基本上都是相通的,学会迁移

    Java中运算符可分为以下:算术运算符(基本+ - * /,增量+= -= *= %=,自增++/自减--)、关系运算符(< > == !=)、逻辑运算符(&& || ! & |)、位运算符(& | ^ ~ )、移位运算符(<< >> >>>)以及条件运算符等。

    算数运算符

    算数异常

    做除法和取模的时候右操作数不能为0,否则会出现“除0异常”

    public class Test {
        public static void main(String[] args) {
            int a = 20;
            int b = 0;
            System.out.println(a/b);
        }
    }

    这里 ArithmeticException 代表这里是一个算数异常的结果,后面 / by zero代表 “除0” ,第二行是定位错误在哪一行。

    取模规则

    public class Test {
        public static void main(String[] args) {
            int c = 7;
            int d = 3;
            System.out.println( c %  d); // 1
            System.out.println(-c %  d); //-1
            System.out.println( c % -d); // 1
            System.out.println(-c % -d); //-1
        }
    }
    • Java 中 % 运算符的规则是:余数的符号与被除数相同。
    • 对于正数,余数就是通常的”除法余数”。
    • 对于负数,余数会按照规则调整,以确保余数的符号与被除数一致,且余数的绝对值小于除数的绝对值。

    而且在Java语言中取模支持运算符两边均为浮点数,和C语言两边均为整数不同。

    public class Test {
        public static void main(String[] args) {
            System.out.println(25.5%2.0); // 1.5
        }
    }

    自增运算符

    如果num为一个已定义的整型变量,形如 num++++num 的表达式中 ++ 代表自增,同理,-- 代表自减。

    如果单独使用这两个效果一样。都是表示为直接+1。而混合使用则分为两种:

    • 前置 ++ :先给变量+1,然后使用变量的值;
    • 后置 ++ :先使用变量原来的值,再表达式结束后给变量+1;
    public class Test {
        public static void main(String[] args) {
            int a = 10;
            System.out.println(a++); // 11
            System.out.println(a);   // 12
        }
    }
    public class Test {
        public static void main(String[] args) {
            int a = 10;
            System.out.println(++a); // 12
            System.out.println(a);   // 12
        }
    }

    关系运算符与逻辑运算符

    C语言中也有相通的《从猜数字程序来理解结构和随机数 – 3.1 Intro》

    关系运算符有> < != == >= <=,返回 truefalse.

    同样的,和C语言一样,形如 0<a<9 多次判断是不能连着写的(已定义整型变量a),要写成 a>0 && a<9 。因为原来的错误写法,如果判断的话,假设 a>0 结果确实是 true 了,但是后面判断就是 true<9 这样是无法比较的。

    public class Test {
        public static void main(String[] args) {
            int a=10;
            System.out.println(a>10);  // false  正确写法
            //错误写法: System.out.println(1<a<11);
            System.out.println(a>1 && a<11); // true 正确写法
            System.out.println(a>10!=true); // true 正确的连续判断写法
        }
    }

    逻辑运算符有与&& (也有单&)或|| (也有单|)非! ,注意与和或操作两边的表达式是结果为boolean类型的表达式。

    • 与:有false出false,全true才true
    • 或:有true出true,全false才false
    • 非:即对立,!true==false !false==true

    逻辑运算符的短路求值

    短路求值就是提前判定,遵循着上述的“有x出x”。

    • 对于 && , 如果左侧表达式值为 false, 则总表达式结果⼀定是 false, ⽆需计算右侧表达式.
    • 对于 || , 如果左侧表达式值为 true, 则总表达式结果⼀定是 true, ⽆需计算右侧表达式.

    所以这样的话,如果左侧表达式值能直接影响整个表达式结果的话,那么提前“短路”。这样即使右边表达式出错,也仍可以输出整个表达式的值。

    public class Test {
        public static void main(String[] args) {
            System.out.println(10 > 11 && 10 / 0 == 0);  // 左侧为false,与逻辑中有false出false,所以提前短路
            System.out.println(10 < 11 && 10 / 0 == 0);  // 左侧为true,那么结果要取决于右侧,右侧表达式错误,所以抛出异常
        }
    }

    如果不希望短路求值,可以使用单 & 表示与和单 | 表示或。 但一般用得频率也比较少。

    位运算符

    Java 中数据存储的最⼩单位是字节,⽽数据操作的最⼩单位是位(bit)。

    位运算符有四个 & | ~ ^,只有 ~ 是一元运算符。

    位操作表⽰按⼆进制位运算,按位运算就是在按照⼆进制位的每⼀位依次进⾏计算。

    按位与、按位或、按位异或

    对于 ^ 异或来说:那就是相同为0,不同为1。(可以想象成“找不同”)

    对于 ~ 按位取反操作来说,可以说是把二进制写出来后逐位取反。

    public class Test {
        public static void main(String[] args) {
            System.out.println(0xa); // 10
            System.out.printf("%x\n",0xa);  //a,以十六进制输出
            System.out.printf("%32s\n",Integer.toBinaryString(0xa)); // 00000000000000000000000000001010
            System.out.println(~0xa);  // -11
            System.out.printf("%x\n",~0xa);  //fffffff5,以十六进制输出
            System.out.printf("%s\n",Integer.toBinaryString(~0xa));  // 11111111111111111111111111110101
        }
    }

    移位运算符

    移位运算符有三个,<< 左移,>> 右移,>>> 无符号右移 (不存在无符号左移)。不常用,了解即可。对于一个正数来说,有如下规律

    • 左移:写出二进制并左移且右边补0,结果为 <原值>*2的<位数>幂次方
    • 右移:写出二进制并右移左边补符号位,结果为 <原值>/2的<位数>幂次方
    • 无符号右移:写出二进制并右移左边始终补0

    条件运算符

    格式 : <条件>?<真时执行>:<假时执行> 同C语言中的。

    运算符优先级

    不好记忆的话,那就加上括号调整优先级吧!

  • 变量详解 | Java语言基础

    变量详解 | Java语言基础

    基本数据类型表

    在前文有提到:《初识Java及数据类型 | Java语言基础》

    基本类型变量通用

    通用格式:<数据类型关键字> <变量名>=<值>

    public class Test {
        public static void main(String[] args) {
            long d = 10L;
            System.out.println(d);
    
            short a = 10;
            System.out.println(a);
    
            byte b = 10;
            System.out.println(b);
    
            float c = 10.24f;
            System.out.println(c);
        }
    }
    • 对于长整型的值来说,需要在数字的末尾加上L(也可以小写,但是大写的方便区分)才能被IDE识别为 long 类型。
    • 对于单精度浮点型的值,需要在数字末尾加上f(也可以大写)才能被默认判断为单精度 float 类型。

    通常,如果查看数据类型可以接受的范围的最大值和最小值,可以利用包装类型的MAX_VALUEMIN_VALUE。一般的,除了 int 的包装类型名为 Integer ,其他的是关键字首字母大写即包装类型名。

    包装类型是基本数据类型中对应的才有的。

    public class Test {
        public static void main(String[] args) {
            System.out.println(Long.MAX_VALUE);
            System.out.println(Long.MIN_VALUE);
            
            //etc.
        }
    }

    整数除法和实数除法

    public class Test {
        public static void main(String[] args) {
            int a = 1;
            int b = 2;
            System.out.println(a/b);
        }
    }

    如上代码输出为0,显然与我们理解的1/2=0.5不同,原因在于除号两边都是整数,相除程序认为就是整数除法,结果自然变成了向下取整的0了。

    如果希望输出预期的0.5这样的浮点数,可以把 a b 变量声明的数据类型改为 double。当然也可以把 a/b 改为 1.0*a/b

    浮点数表示得不精确?

    public class Test {
        public static void main(String[] args) {
            double a = 1.1;
            System.out.println(a*a);
        }
    }

    如上代码输出为1.2100000000000002 。 为什么小数点后面会这么长呢?是因为浮点数本来就不是一个精确的数字,计算机也无法表示精确是因为计算机以二进制的方式存储浮点数时,内存布局遵守 IEEE 754 标准(和C语⾔⼀样),尝试使⽤有限的内存空间表⽰可能⽆限的⼩数,势必会存在⼀定的精度误差。

    字符型可以存的字符

    不同于C语言中字符型变量只能存一个单字母,Java语言中的字符型不仅可以存储字母,还可以存储中文的单字。同样的,都需要单引号 '' 把字符括起来。

    在Java语言中字符的底层是Unicode,其中也包含了ASCII的部分,且码值原样不变。也正是底层为Unicode字符集,所以能表示的字符比C语言更多并包含中文。

    字符型的包装类型名是 Character ,但是其MAX_VALUEMIN_VALUE 都是不可打印的字符。

    字符型在计算机中本质上是个整型,可以用如下代码检验:

    public class Test {
        public static void main(String[] args) {
            int ch2int = 'a';
            System.out.println(ch2int);
    
            char int2ch = 'a';
            System.out.println(int2ch);
        }
    }

    布尔型的严格定义

    public class Test {
        public static void main(String[] args) {
            boolean yes = true;
            boolean no = false;
        }
    }

    如上 yes 和 no 两个变量可以赋的值只能是布尔值中的真( true )假( false ),Java语言中没有隐式地将数字(隐式就是如C语言的”非零为真,0为假”)转换为布尔值

    Java虚拟机规范中,并没有明确规定boolean占⼏个字节,也没有专⻔⽤来处理boolean的字节码
    指令,见官方文档的引用:

    「Chapter 2. The Structure of the Java Virtual Machine – The boolean Type」
    In Oracle’s Java Virtual Machine implementation, boolean arrays in the Java programming language are encoded as Java Virtual Machine byte arrays, using 8 bits per boolean element.

    类型转换

    自动类型转换(隐式)

    代码不需要经过任何处理,在代码编译时,编译器会⾃动进⾏处理。整数默认为int整型,浮点数默认为double双精度。

    特点:数据范围⼩的转为数据范围⼤的时会⾃动进⾏。

    比如让一个long类型的变量赋值为int类型变量:

    public class Test {
        public static void main(String[] args) {
            int a = 10;
            long b = a;
            System.out.println(b);
        }
    }

    强制类型转换(显式)

    当进⾏操作时,代码需要经过⼀定的格式处理,不会自动处理。

    如果数据范围大的类型转化为数据范围小的,利用强转出现两种情况:如果范围大的类型的数字本来就在范围小的类型内的话,其实截断后没有影响;但是范围大的类型的数字还大又超出了范围小的类型可接受的数值范围,则会造成丢失数据(或者说精度丢失),但仍可输出(IDE也会有检查操作),因为你的强转操作本身就代表了你接受丢失数据的后果。

    不相⼲的类型不能互相转换。

    类型提升

    不同类型的数据之间相互运算时,数据类型⼩的会被提升到数据类型⼤的。

    public class Test {
        public static void main(String[] args) {
            int a = 10;
            long b = 256l;
            long c = a + b;   // yes
            int d = a + b;  // no
        }
    }

    但是也有个同类型运算出现的奇怪现象,这个类型就是byte

    public class Test {
        public static void main(String[] args) {
            byte a = 10;
            byte b = 1;
            byte c = a + b;   // no
            int d = a + b;   //yes
        }
    }

    虽然a和b都是byte类型,但是计算时候会把a和b均自动提升为int,导致结果也需要int。这是由于计算机CPU是通常按照4个字节为单位从内存中读写数据,以此为了硬件上实现的方便。short类型也同理。

    字符串类型 – 引用数据类型

    字符串的关键字为 String ,注意大写。

    字符串拼接与字符串和整数互转

    两个字符串相加的效果就是字符串拼接。

    public class Test {
        public static void main(String[] args) {
            String str = "Hello";
            String str2 = " world!";
            System.out.println(str+str2);
        }
    }

    有时需要整数和字符串拼接,即使单个整数加入到字符串可以采用 System.out.println("a =" + 10); 但是如果希望加号后面是一个表达式呢? 可以对表达式加个括号,System.out.println("a =" + (a+b));

    又或者可以使用String类类型的功能:String str = String.valueOf(<数字或结果为数字的表达式>); 使得整数转为字符串。

    反过来,可以使用 int num = Integer.parseInt(<数字字符串>);

    public class Test {
        public static void main(String[] args) {
            int a = 10;
            String str = String.valueOf("a = " + a);
            System.out.println(str);
            int b = 100;
            String str2 = String.valueOf("a + b = " + (a + b));
            System.out.println(str2);
    
            String stra = "10";
            int a2 = Integer.valueOf(stra);
            System.out.println(a2-1);
        }
    }
  • 初识 Java 及数据类型 | Java语言基础

    初识 Java 及数据类型 | Java语言基础

    之前发过一期简单的入门《 Java 上的HelloWorld,怎么这么多细节?》 ,接下来将会是系统地学习Java之旅~

    JavaSE & JavaEE

    Java Standard EditionJava Enterprise Edition
    开发桌面应用和简单服务器程序用于开发大型、分布式企业应用和Web应用
    核心语言特性和基本API(如I/O、网络、GUI)企业级API(如Servlet、JSP),特性更广泛、复杂
    适合小型和中型应用适合大型应用
    可在标准JVM上运行需要特定的应用服务器

    Java的发展历史

    Java之父 —— 詹姆斯·高斯林(James Gosling),Sun 公司(后被Oracle收购)

    1995年PC互联网时代,5月时以Java名称发布,提出“Write once, run anywhere.”口号。

    能实现跨平台的主要原因就是编译后的 *.class 文件,可以跨平台运行。

    JDK安装和后续文章环境说明

    • 环境:JDK安装路径不要有中文,以后装软件里面也不要有中文。如果是多个Java版本请设置PATH指定命令行中使用的开发环境版本(如果是IDE内,直接软件内切换即可)
    • 集成开发环境IDE:IDEA Community 2022.1.4(社区版)免费,软件寻找:Bing搜索idea→IntelliJ IDEA→“下载”按钮→软件图片右下角“其他版本”→看第二列Community部分的,下拉找到2022.1.4中 2022.1.4 – Windows x64 (exe)
    • 后续文章中的环境(如果没有特殊说明的话):
      Windows 11、 JDK17(17.0.9)、IDEA Community 2022.1.4
    • 如果向他人请教Java编程中的疑问,请先附上自己的环境~

    PATH配置路径:(Windows 11)右键任务栏的开始选择设置→系统→下拉到最后找到设备信息→相关链接中的高级系统设置→高级选项卡中最下面的环境变量,双击其中的Path进入“编辑环境变量”窗口,新建 %JAVA_HOME%\bin; (其中博主右多个Java版本所以加了数字进行区分方便切换环境,新建的这个路径格式应该是 %变量名%\bin ,还需在“环境变量”窗口下再点击一次“新建…”从而新建名为JAVA_HOME的变量,值为JDK安装的路径)

    环境变量设置

    HelloWorld I/O

    更多可以联系前文:《Java上的HelloWorld,怎么这么多细节?》

    流程

    ① 写代码 → 保存为 HelloWorld.java

    public class HelloWorld{
        public static void main(String[] args) {
            System.out.println("HelloWorld");
        }
    }

    ② cmd内编译 (javac.exe)→ 生成字节码文件 HelloWorld.class

    javac HelloWorld.java

    ③ cmd内执行输出

    java HelloWorld

    运行在JVM(Java虚拟机)上,包含在JDK中。

    【面试考法】JDK、JRE、JVM区别?

    • JDK:Java开发⼯具包(Java Development Kit),提供给Java程序员使⽤,包含了JRE,同时还包含了编译器javac与⾃带的调试⼯具Jconsole、jstack等。
    • JRE:Java运⾏时环境(Java Runtime Environment),包含了JVM,Java基础类库。是使⽤Java
      语⾔编写程序运⾏的所需环境。
    • JVM:Java虚拟机,运⾏Java代码
    Java IDE、JDK、JRE、JVM各层级关系

    各部分概念带过

    public 修饰符class 定义类的关键字Test 类名
    publicstatic 声明类的静态成员void main(String[] args) 方法

    命令行参数 String[] args

    用一个测试案例说明可以借助该参数输入一些值:

    public class HelloWorld{
        public static void main(String[] args) {
            for (int i=0;i<args.length;i++){
                System.out.println(args[i]);
            }
        }
    }
    java HelloWorld nibbles hello world!
    // 输出:
    // nibbles
    // hello
    // world

    Java代码文件的层级

    代码层级示意图
    • class 是类,在⼀个代码⽂件中只能有⼀个public修饰的类,⽽且代码⽂件名字必须与public修饰的类名字相同。
    • 方法即为函数,main() 依然是程序的入口
    • 代码规范:注意左花括号的位置

    注释

    //单行注释
    /*多行注释*/ 
    /** ⽂档注释 */ 
    
    /**
    文档注释:
        @version v1.1.0
        @author nibbles
        HelloWorld practice
    */
    //javadoc命令用于生成文档注释
    javadoc -d myHello -author -version -encoding UTF-8 -charset UTF-8 HelloWorld.java

    字符集不对的报错

    cmd报错截图

    如果说需要在注释中或输出中包含中文,需要保持文件编码和cmd命令行编码的统一,否则会在编译时候出现如上错误。比如都设置为GBK,见下:

    如何查看cmd窗口的代码页编码?

    右键cmd窗口标题栏,属性→当前代码页

    不推荐更改代码页编码,其更改注册表比较繁琐。文件转编码其实更快。

    如何按指定编码保存?

    以VSCode编辑器为例,

    或者在写编译命令时,加上encoding参数 (表示文件是以UTF-8保存的,那么编译时候解码就如此)

    javac HelloWorld.java -encoding utf-8

    使用IDE就可以避免这个情况。

    注释规范

    1. 内容准确:注释内容要和代码一致匹配,并在代码修改时及时更新
    2. 篇幅合理:注释既不应该太精简,也不应该扯车轱辘话长篇大论
    3. 使用中文:一般中国公司都要求使用中文写注释,外企另当别论
    4. 积极向上:注释中不要包含负能量

    标识符

    给类名、⽅法名或者变量所取的名字。标识符中可以包含:字⺟、数字以及 下划线和 $ 、_ 符号等(不可以加冒号)。但不能以数字开头,也不能是关键字,且严格区分⼤⼩写。

    驼峰命名建议

    • 类名:每个单词的⾸字⺟⼤写(⼤驼峰) ,如 Test
    • ⽅法名/变量名:⾸字⺟⼩写,后⾯每个单词的⾸字⺟⼤写(⼩驼峰),如 studentName

    未来就业时候视入职公司的发的代码规范/浏览的之前的代码而定,一学代码规范二学业务。

    关键字

    由Java语言提前定义好的,有特殊含义的标识符或保留字。

    各用途关键字

    IDEA高效开发

    快捷补全语句(输入后按tab)

    • psvmmainpublic static void main(String[] args){}
    • soutSystem.out.println()

    运行 (点击框选的 即可)

    IDEA截图

    数据类型

    打印语句和字面常量

    public class Test {
        public static void main(String[] args) {
    
            // 输出123后自动换行,println或print可以接受任意类型
            System.out.println(123);
    
            // 去掉ln则输出末尾不换行
            System.out.print(123);
    
            // 类似于C语言中的printf("%d\n",123);格式化字符串的写法。【用得少】
            System.out.printf("%d",123);
        }
    }

    字面常量概念同其他语言,就是固定不变的量,分字符串常量(双引号””括起来的)、整型常量、浮点数常量、字符常量(单引号”括起来的)、布尔常量(true或false)、空常量(null)。

    数据类型

    字符串、整型、浮点型、字符型以及布尔型,在Java中都称为数据类型。

    八种数据类型的关键字、内存占用、范围

    附注:

    • 内存占用的字节数不因电脑操作系统的32位还是64位的改变而改变
    • 没有像C语言一样分有符号还是无符号的概念,正负均可表示

    变量

    同C语言中的变量定义方法。

    public class Test {
        public static void main(String[] args) {
            //<数据类型关键字> <变量名>=<值> 
            int num = 10;
            System.out.println(num);
        }
    }

    尤其注意要把局部变量(定义在方法内的变量叫局部变量)还要初始化,不能简单的一个 int num; ,否则会导致编译就报错

    对于int类型的变量,四个字节中最高位是符号位(1bit),0为正,1为负。后面31个bit(位)才真正代表数值大小。所以如果对于1字节内7bit数值位的话,可以表示范围也就是 -2^7~(2^7-1)即-128~127。

    则对于-128 转化为 二进制源码为 1 1000 0000;反码为 1 0111 1111(符号位不变);补码(=反码+1)为 1 1000 0000。

    范围验证:

    public class Test {
        public static void main(String[] args) {
            System.out.println(Integer.MAX_VALUE); // Integer属于包装类型,此语句可以输出int类型的最大值
            System.out.println(Integer.MIN_VALUE); // 此语句可以输出int类型的最小值
        }
    }

    在直接赋值常量值的情况下,如果给变量的值超出类型本身可接受的范围,也会在编译时候直接报错。

  • 从本质理解指针的那些知识 | C语言基础

    从本质理解指针的那些知识 | C语言基础

    指针在C语言中是个很重要的概念,我更多从类比并结合计算机硬件的角度开始引入概念。

    我也是初学者,指针也确实是个很容易被误解的概念,请多指教,评论区留言误区一般时间1h内会回。

    1. 引入指针

    1.1 简要理解

    在之前的《谈谈printf()和scanf() | C语言基础》文章中,讲到变量的时候说过,变量每次创建同时会在内存中分配其一个空间。

    我们从高中时期或者更早就了解过计算机硬件的简单知识,如下导图(=>完整版指路)。

    硬件基础思维导图

    我们知道计算机上CPU(Central Processing Unit 中央处理器)在处理数据的时候,会从内存中读取数据,处理后的数据也会放回内存中。内存中为了高效管理空间,会把内存分为一个个的内存单元,一单元一字节,且每个单元都有自己的既定的地址。

    更简单地,可以把内存想象成为一栋按房间编号管理的宿舍楼,然后每个“房间”就容纳一个字节,如果寝室内发生什么事儿,门牌号就是宿管找到你们寝室的地址。

    所以类比下,当CPU需要一段数据时候,必须要有个准确的门牌号,否则就无从找到目标。C语言中这个门牌号就叫做“指针”,指针你可以看作地址的另一个叫法。

    1.2 拓展:从硬件角度理解 【想了解的可以点此展开】

    一般理解到这里就差不多了,不过也许有人觉得这样解释地还是过于简单,那从再从硬件角度来看指针是如何起作用的(博主大一还没学计算机组成之类的课,仅为自主课外拓展和理解)。

    硬件角度图解参考

    当我们写下 int* pa = &a; 并执行时,(假设 a 是前文已经定义过的变量,基础知识回顾:&是取地址操作符,故 &a 即代表 a 变量存储在内存中的地址),编译器会把 pa (指针本身)的数值写入CPU的MAR(寄存器)中。

    随后地址总线将这一串二进制位送往内存控制器,内存条内部的译码电路接收到地址后找到确切的内存单元。接着数据总线把地址 &a 存储到MDR,再将其返回寄存器或缓存。

    部分概念再类比上述例子,按执行顺序排列:

    • Memory Address Register (MAR):CPU 内部的寄存器,用来暂存即将访问的内存地址;相当于把“门牌号”写在便签上准备递给内存系统。一般为单向的(CPU → 内存/IO)。
    • 地址总线 (Address Bus):把 MAR 中的地址位输出到主板/芯片上的地址导线上,送往内存控制器。如同走廊/道路网络,负责把门牌号传递到内存。(现代主流芯片物理地址线 39–52 位,但方便说明)假设其线的条数=CPU的字长(64位或32位,32位有时也表达为x86),比如32位机器下常见有32根地址总线产生的2进制序列作为一个地址,一个地址有32位,常需要4个字节。(32bit=4Byte)存储。
    • 行列译码器:含译码电路,在内存芯片内部决定“第几层楼、第几号房”,锁定内存单元。
    • 数据总线 (Data Bus):把目标单元中的数据从内存传回 CPU,也可以在写操作时把数据送入内存。是种双向的总线(读时内存 → CPU,写时 CPU → 内存)。
    • Memory Data Register (MDR):又叫 Memory Buffer Register (MBR),也是一种CPU内部的寄存器,负责在 CPU 与内存之间暂存刚读到的数据或待写出的数据;可将其看作各个房间之间投递的快递员的手中的包裹。
    • 缓存(Cache):热数据暂存地,加快访问(若命中缓存,即可直接MAR → MDR),但不影响“地址=定位信息”的本质。

    从本质上讲,指针只是一串能够唯一定位某个内存单元的地址数字(比如x64下Debug时形如0x000000CD3450F6E4的十六进制数)。它本身不保存数据,而是告诉程序“你要的数据在哪里”

    2. 指针变量

    2.1 基本格式和大小

    在 C 语言里,我们仍然需要一个变量来存放这串地址数字,这样的变量我们称之为“指针变量”。

    #include <stdio.h>
    
    int main()
    {
        int a = 10;
        int* pa = &a;
        printf("%p", pa);
    }

    如上,声明时候的* 号在说明 pa 即为指针变量,指针变量内保存的是 a 变量的值的地址,然后通过 printf() 来打印出来。

    如下,a 变量赋值为10,&a定位到地址发现已经被存入了内存中(00 00 00 0a,逆序,这里的a是代表十六进制表达下的10)。也就是 pa 并不直接持有 10 整数的值,而是保存了 a 所在内存单元的地址。
    VS中内存窗口

    你可能会好奇,那指针变量是存的地址,每个地址的大小在机器里就已经固定好了的,那么指针变量大小和地址大小一样,为什么还要在声明指针变量的时候去写上类型呢?

    指针变量声明时的类型图解

    原因在于:当我们沿着地址取数时,必须事先知道打算读取多少字节。int*
    “告诉”编译器,一步跨四格;char* “告诉”它,一次只走一格。这样既能保证数据对齐,也能避免读写越界。

    后文 2.2指针操作中的“± 整数值”部分也有讲解。

    一次走几格,我们来看看指针变量的各种操作。

    2.2 指针操作/应用

    解引用操作符 *

    又称间接寻址运算符。这个主要是为了解决找到地址指向的对象。你看我们存地址也是有需要取出来用的时候。C语言中利用解引用操作符 * ,来通过已知指针变量找到其指向的变量。

    看下面的案例,注意只有在指针变量名前面加的 * 才算是解引用操作符。int* 中的 * 不是。

    #include <stdio.h>
    int main()
    {
        int a = 10;
        int* pa = &a;
        *pa = 0;
        printf("%d", a);
    }

    如上这个很简单的案例,其中*pa 就是通过 pa 指针变量中存放的地址,找到指向的 a 变量。而且可以利用此方法间接修改了a 变量的值,此代码执行后输出0。

    这样的间接修改的方式,相比直接 a = 0; 来说,对变量的调用等操作更加灵活。

    注意:在 C 里进行解引用必须满足以下前提——

    • 指针非空(不为 NULL);
    • 指针指向的地址处于程序已分配的有效内存范围;
    • 解引用类型与目标对象匹配,以免因字节对齐或大小差异造成未定义行为。

    ± 整数值

    这个主要是为了解决上文2.1末说的“一步跨几格”的问题。看下示意图 ptr-2 的部分更易理解:
    加减整数值效果图解

    #include <stdio.h>
    int main()
    {
        // [模拟Debug逐行执行过程]
        int ages[4] = { 0, 1, 2, 3 };  
        // 创建数组ages: { 0, 1, 2, 3 }
        int* ptr = &ages[2];  
        // ptr是ages数组第三个元素的指针变量
        *ptr = 8;  
        // 修改第三个元素为8 => 数组ages: { 0, 1, 8, 3 }
        ptr++;
        // ptr是ages数组第四个元素的指针变量
        *(ptr - 2) = 9;
        // 修改第二个元素为9 => 数组ages: { 0, 9, 8, 3 }
    }

    按上面这个案例,因为数组元素在内存中是连续存放的。指针往前移动一步两步是直接由加减后面的整数来得知的,非常显然。

    但是如果从其十六进制地址数字来看,地址的数字又增加了多少呢?

    #include <stdio.h>
    int main()
    {
        int n = 10;
        int* pi = &n;  
        // 原先int类型的地址赋给指针变量pi
        char* pc = (char*)&n; 
        // 强转int类型的地址为char类型的地址,赋给指针变量pc
    
        // 自C99标准起,把指针强转为 void* 才是完全符合标准
        // 往右滑,有对齐的便于观察的输出结果
        printf("  &n   = %p\n", (void*)&n);     // Output:   &n   = 00AFF974
        printf("  pi   = %p\n", (void*)pi);     // Output:   pi   = 00AFF974
        printf("pi + 1 = %p\n", (void*)pi + 1); // Output: pi + 1 = 00AFF978
        printf("  pc   = %p\n", (void*)pc);     // Output:   pc   = 00AFF974
        printf("pc + 1 = %p\n", (void*)pc + 1); // Output: pc + 1 = 00AFF975
    }

    我们看到原来的 int* 声明的指针变量 pi 的十六进制地址 ,在 pi + 1+4sizeof(int)),这就是“一步跨四格”;原来的 char* 声明的指针变量 pc,在 pc + 1+1(sizeof(char)),这就是“一步跨一格”。

    指针 – 指针

    实际应用案例比如在数组中可以表示元素个数差,也可以在数组中限定要遍历的范围。(如下)

    在算法场景中也有用于“快慢指针”算法的长度统计等等。

    #include <stdio.h>
    
    int main()
    {
        int nums[5] = {10, 20, 30, 40, 50};
        int *left  = &nums[1];   // 左指针
        int *right = &nums[4];   // 右指针
    
        // gap元素个数差
        int gap = right - left; 
        printf("gap = %d\n", gap); 
        //Output: gap = 3
    
        // 用左右指针遍历区间 [left, right) 
        for (int *p = left; p != right; ++p)
        {
            printf("%d ", *p);      
        }              // Output:20 30 40
        puts("");      // 换行
    }

    注意:两个指针必须指向同一个连续对象(通常是同一数组,或者刚好其一指针指向结尾位置)才能安全相减,否则跨数组或随意相减会导致未定义行为。

    函数内实现两个值的改变

    比如说写一个交换函数 swap()

    #include <stdio.h>
    void swap(int*, int*);
    int main()
    {
        int a=3, b=5;
        swap(&a, &b);
        printf("%d, %d\n", a, b);
    }
    void swap(int *p1, int *p2)
    {
        int t;
        t = *p1;
        *p1 = *p2;
        *p2 = t;
    }

    实际上,这样的值传递,地址没有改变,存放的变量的值改变了。

    总结下要使某个变量的值通过函数调用发生改变:

    1. 在主调函数中,把该变量的地址作为实参
    2. 在被调函数中,用形参(指针)接受地址
    3. 在被调函数中,改变形参(指针)所指向变量的值

    3* 写在最后:

    Q:为什么我还是继续在更C语言内容的原因?

    A:首先我们学校的进度偏慢吧,而且我自己也对这部分的知识点的学习发散得也多。(我后续的学习计划会加入Java,静待更新吧~)

    由此就会有很多的知识堆积在脑海中,我希望通过费曼的方式把这些知识用公众号(博客)文章的形式进行结构化输出。

    所以其实我不是一个技术大牛,也是个不断学习者。如果你有发现什么错误,欢迎留言!我将速速赶到然后与你交流技术话题。

  • 从猜数字程序来理解结构和随机数 | C语言基础

    从猜数字程序来理解结构和随机数 | C语言基础

    由于博主过去学过Python,本篇文章中会有很多对照的分析。
    本次为了更利于各部分的理解,我们从一个简单的猜数字程序的实现来说起…

    1. 程序实现流程图:

    程序实现流程图

    2. 随机数生成

    Python中生成随机数的方式,可以利用内置的 random 模块进行生成。而且每次文件运行时输出的随机数都是不同的,同一文件内若多次调用随机数函数也都是不同的。

    如下就是生成并打印1~100以内的整数的Python程序代码:

    import random
    randomNum = random.randint(1,100)
    print(randomNum)

    对于C语言中,如果要实现随机数,会稍显复杂。

    首先,需要 rand() 函数实现伪随机数,为什么说是“伪”呢?是因为每次运行文件内的多个 rand() 函数时候之后的几次都和第一次是一个值。

    伪随机数示范

    说明这样的“随机”有可能是经过某种算法得到的,而不是真的随机。调查后,事实上 rand() 函数每次都是对一个叫“种子”的基准值运算后生成的。那么我们就会想“种子”是如何得出的?根据什么来算出的?

    根据此,可以想到用 srand() 函数来初始化随机数的生成器,通过 srand() 函数的 seed 参数来设置需要 rand() 函数生成随机数的“种子”。也就是说 srand() 的“种子”如果是随机的, rand() 就能生成随机数,但是生成随机数的前提是还需要一个随机的值,这样的矛盾又该如何解决呢?我们需要找到一个时刻在变的值来依据此得出这个需要的随机值。

    那么我们可以使用程序运行的时间来作为“种子”,那么我们可以再引入 time() 函数。 time() 函数返回的类型是 time_t 类型(本质上还是32位或64位的整型)的时间戳值。时间戳是1970年1月1日0时0分0秒到程序运行时候的时间差,单位是秒。time() 函数中的参数 timerNULL 即可返回差值。time_t 类型对于 srand() 函数是不接受的,所以强制类型转化 unsigned int

    综上(其实上面的你没听懂也没关系),我们可以写出生成随机数的方式了。

    注意需要新引入三个库:stdio.hstdlib.htime.h

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main(){
        srand((unsigned int)time(NULL));
        printf("%d\n", rand());
    }

    那么为了猜数字的范围缩小为1~100,我们可以对rand()函数结果对100取模。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main(){
        srand((unsigned int)time(NULL));
        printf("%d\n", rand() % 100 +1);
    }

    3. 分支结构

    3.1 Intro:关系操作符+逻辑操作符

    • 关系操作符
      • >(大于)
      • >=(大于等于)
      • <(小于)
      • <=(小于等于)
      • == (等于):特别注意这里是两个等号,在很多计算机语言中都是双等号表示“等于”(或是相同、相等)的判定符号
      • != (不等于)
    • 逻辑操作符
      • && (与):两边均为true才输出true,有一个false结果一定是false。(记忆方式:数字电路的方框形逻辑门中间的符号就是& )
        与逻辑示意图
      • || (或):两边其中一个为true就输出true,两边均为false才输出false。(记忆方式:数字电路的方框形逻辑门中间大于等于号下面是两条线的)
        或逻辑示意图

    这些运算符的结果与算术运算符算出实际的结果不同,而是返回1(true)和0(false)整型值,以此来控制我们的程序接下来选择Y(Yes符合条件)的分支还是N(No不符合条件)的分支。

    而且这些操作符都是双目的,也就是符号两边都需要有值。

    注意与Python的符号表达不同的一点是,Python里 A<x<Bx>A and x<B 是实现的一个效果,都是把x值限定在(A,B)区间内,而C语言中对应的方式最好是 x>A && x<B,另一种形式( A<x<B )虽然不会报错,但对于C语言程序来说,执行时会有误解,实际上执行的是 (A<x)<B ,那么就是先判断A和x,然后才是把前面判断的结果和B再做判断,通常会达不到想要的结果。

    3.2 if-else

    #include <stdio.h>
    int main()
    {
        int age = 0;
        scanf("%d", &age);
        if (age >= 18) {
            printf("Adult\n");
        } else {
            printf("Child\n");
        }
    }

    如上是个很简单的年龄判断的 if-else 分支结构,结构也类似于Python里的。如果输入的age值是大于等于18就输出Adult,其他情况(也就是 age 值小于18)就输出Child。

    那么还可以用嵌套的结构,能实现如果两个if条件+其他的三种分支。

    #include <stdio.h>
    int main()
    {
        int score = 0;
        scanf("%d", &score);
        if (score >= 90) {
            printf("Great!\n");
        } else if (score >= 60) {
            printf("Pass\n");
        } else {
            printf("Failed\n");
        }
    }

    这里的 else if 就是类似于Python中的 elif ,这里其实可以看作是两个 if-else ,第一个结构中的 else 部分嵌套入了第二个结构。

    如上,我们可以简单写出猜数字程序中的用分支结构表示的比大小的部分。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        srand((unsigned int)time(NULL));
        int randomNum = rand()%100+1;    // 随机数范围 1~100
        int num = 0;                   // 用户输入的猜测数
        printf("请输入 1-100 之间的数字:");
        scanf("%d", &num);
        if (randomNum > num) {
            printf("猜小了\n");
        }
        else if (randomNum == num) {
            printf("猜对啦\n");
        }
        else {
            printf("猜大了\n");
        }
        return 0;
    }

    需要提一下的是,C语言的 scanf() 虽然在功能上基本和Python中的 input() ,但是在输入的是字符串的情况下,不支持读取带空格的字符串。

    3.3 条件操作符

    还有种方式可以表达这样的分支结构,不过更多地是用于双分支或者三分支。但是三分支的实现如果要说代码的可读性上考虑,不太推荐使用。

    基础的双分支的条件操作符的结构为:

    逻辑或关系表达式 ? 真值表达式 : 假值表达式

    就是那么简洁,一个问号和冒号就连接了三个表达式。来看极简的两数求最大的案例:

    int max = (a > b) ? a : b;

    那么我们上面的判断部分的如果要写成三分支(不推荐,为了清晰起见按行分层缩进了):

    printf("%s\n",
           (randomNum > num)   ? "猜小了":
           (randomNum == num)  ? "猜对啦":
                               "猜大了");

    双分支:

    if (randomNum == num){
        printf("猜对啦");
    } else {
        printf("%s\n", (randomNum > num) ? "猜小了" : "猜大了");
    }

    4. 循环结构

    猜数字游戏中我们还希望做回合数( round )的限制,如果是猜错(即输出“猜大了”“猜小了”这两种)情况下,可以消耗回合数再猜一次。如果可猜的回合耗尽,则输出“GameOver”结束游戏。

    4.1 while

    C语言中非零即为true,零直接是false,可以借此作为循环次数的控制条件。设定好了我们总回合数,那么每执行完一次判断则可以直接减去一次回合数。当回合数为0正好可以结束循环条件。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        srand((unsigned int)time(NULL));
        int randomNum = rand() % 100 + 1;   // 随机数范围 1~100
        int num = 0;                      // 用户输入的猜测数
        int round = 5;                    // 最大回合数
    
        // while 循环控制回合,从 round 到 1
        while (round) {
            printf("请输入 1-100 之间的数字:");
            scanf("%d", &num);
            if (num == randomNum) {
                printf("猜对啦\n");
                break;
            } else if (num < randomNum) {
                printf("猜小了\n");
            } else {
                printf("猜大了\n");
            }
            round-=1;
        }
        if (round == 0) {
            printf("GameOver\n");
        }
        return 0;
    }

    不过,while循环一般更多用于未知循环次数的情况时,比如在刷题的时候经常会看到不知道会输入多少数代表输入终止(EOF)。

    ▼ 牛客网OJ上的初始代码

    #include <stdio.h>
    int main() {
    &nbsp; &nbsp; int a, b;
    &nbsp; &nbsp; while (scanf("%d %d", &a, &b) != EOF) { // 注意 while 处理多个 case
    &nbsp; &nbsp; &nbsp; &nbsp; // 64 位输出请用 printf("%lld") to
    &nbsp; &nbsp; &nbsp; &nbsp; printf("%d\n", a + b);
    &nbsp; &nbsp; }
    &nbsp; &nbsp; return 0;
    }

    4.2 for

    for 循环就是常用于我们的已知循环次数的程序里了。

    我们直接来看两个最常用的Python和C语言同样实现迭代的代码,这样看是不是好理解了。

    for count in range(1, round + 1):
        print("这是第", count, "次循环")

    Python中使用 range() 生成的整数序列作为可迭代对象,这个整数序列的区间是 [1, round] 末尾不取,函数默认的步长是1,实现效果就是把count变量的值不断从1取到round。

    C语言中的对于起始值、终止条件、步长各部分用分号断开,这基本的三要素还是非常明显的。

    for (int count = 1; count <= round; count++){
        printfprintf("这是第 %d 次循环\n", count);
    }

    那么我们猜数字程序用 for 循环就能这样写:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        srand((unsigned int)time(NULL));
        int randomNum = rand() % 100 + 1;   // 随机数范围 1~100
        int num = 0;                      // 用户输入的猜测数
        int round = 5;                    // 最大回合数
    
        // for 循环控制回合,从 1 到 round
        for (int count = 1; count <= round; count++) {
            printf("第 %d 轮,请输入 1-100 之间的数字:", count);
            scanf("%d", &num);
            if (num == randomNum) {
                printf("猜对啦\n");
                break;
            } else if (num < randomNum) {
                printf("猜小了\n");
            } else {
                printf("猜大了\n");
            }
            if (count == round) {
                printf("GameOver\n");
            }
        }
        return 0;
    }

    5. 补充的分支与循环结构

    C语言中还有些别的分支和循环结构,接下来会一一道来。

    5.1 switch 分支

    switch 语句像一个分流路口:给它一个整数(或枚举、字符),它就把程序送到对应的 case 代码块,比长串 if‑else if 好读得多。

    写法发生效果
    switch (表达式)先算出表达式的值,再去找相同数字的 case
    case 常量:对上号就从这里开始执行。
    default:前面都不对时走这里,可省略。
    break;结束 switch,跳到大括号后面的第一条语句。
    如果不写的话,

    break 和 continue

    • break:立刻结束当前最近的一层 forwhiledo‑while 循环或 switch 的条件分支,程序跳到循环后面的第一条语句。
    • continue:立刻结束本次迭代,把控制权送回循环头;for 会先执行增量表达式再检查条件,while/do‑while 会直接检查条件。在不套循环的 switch 里敲 continue,编译器会直接报错。
    • 在嵌套循环中,它们只作用于最内层。

    只打印 1‑10 中的奇数,遇到 7 就提前停止:

    #include <stdio.h>
    
    int main() {
        for (int n = 1; n <= 10; ++n) {
            if (n == 7)
                break;          // 终止整个循环
            if (n % 2 == 0)
                continue;       // 跳过偶数,继续下一轮
            printf("%d ", n);
        }
        puts("循环结束");
        return 0;
    }
    
    //Output:
    //1 3 5 
    //循环结束
    • continue 让偶数分支直接回到 for 的自增步骤。
    • break 在遇到 7 时立即离开循环,剩余数字不再检查。

    这样,你就能直观看到两条语句在循环里的“停一停”( continue )与“走人”( break )效果。

    5.2 do‑while 循环

    do‑while 像一次先行动、后条件判断的循环:不管条件真假,主体最少执行一次

    写法发生效果
    do { ... } while (条件);先跑 do { ... },再判断 while (条件);条件为真就回到 do 继续,为假就结束循环。

    例如,累加直到用户输入 0:

    #include <stdio.h>
    
    int main() {
        int n, sum = 0;
        do {
            printf("输入数字(0 结束): ");
            scanf("%d", &n);
            sum += n;
        } while (n != 0);
        printf("总和为 %d", sum);
        return 0;
    }

    PS:这并非是一定要使用do-while循环才能实现。

    5.3 goto 不定转移

    goto 是一句直接跳转:给它一个标签名,程序立刻冲过去执行标签下面的代码。

    写法会发生效果
    <label>:普通语句前加标签名,供 goto 跳入。
    goto <label>;立即跳到同函数内的 label: 处。

    比如下面这个分数输入的代码,遇到输入检查失败就跳到清理位置:

    #include <stdio.h>
    
    int main(void) {
        int score;
        printf("请输入分数(0~100): ");
        if (scanf("%d", &score) != 1)
            goto ERROR;
        if (score < 0 || score > 100)
            goto ERROR;
        printf("成绩合法,分数是 %d", score);
        return 0;
    
    ERROR:                        
        printf("输入非法,程序结束。");
        return 1;
    }

    goto 语句如果使⽤的不当,就会导致在函数内部随意乱跳转,打乱程序的执⾏流程,所以我们的建议是能不⽤尽量不去使⽤;更多情况推荐 breakreturn 或把逻辑拆成函数更易读。

  • 谈谈printf()和scanf() | C语言基础

    谈谈printf()和scanf() | C语言基础

    不同于Python,直接就可以使用类似的print()input(),C语言中这两个库函数都需要引入最基础的表示输入输出的stdio.h头文件
    #include <stdio.h>

    博主环境使用Windows 11,C语言IDE大多用的是Visual Studio 2022,校内使用CFree5;
    按认识顺序撰文,从helloworld认识printf(),而后认识scanf()

    1. printf() 打印输出

    相比于博主之前学过的Python语言中的print(),C语言中的printf(),从函数名上多了个f字母,代表format(格式),说明可以定制输出⽂本的格式,这文本格式也叫数据类型。

    数据类型

    Intro

    参考文档:https://zh.cppreference.com/w/c/language/arithmetic_types#.E6.95.B0.E6.8D.AE.E6.A8.A1.E5.9E.8B

    sizeof() 函数

    sizeof()是个用于计算操作符类型长度的函数,输出的单位为字节(Bite)(换算:1Bite=8bit)
    虽然输出的单位是字节,看着输出类型好像是整数,但是根据实际的编辑器输出的附加提示可得这是size_t类型。所以printf中的占位符应该是%zd

    #include <stdio.h>
    int main()
    {
        printf("%zd",sizeof(int));  // 输出:4(表示int类型的长度为四个字节)
        return 0;
    }
    根据头文件来查数据类型对应的取值范围
    • limits.h ⽂件中说明了整型类型的取值范围。
    • float.h 这个头⽂件中说明浮点型类型的取值范围。(有的编译器下可能没有写)
      取值范围的极限值都是定义在头文件中的一些常量里的,比如INT_MININT_MAX代表int数据类型的最⼩值和最⼤值,这些我们叫宏常量
    // 节选自limits.h
    
    #define INT_MIN &nbsp; &nbsp; (-2147483647 - 1)
    #define INT_MAX &nbsp; &nbsp; &nbsp; 2147483647

    integer 整型

    • short:长度为2,范围为 -32767~32767
    • int:长度为4,范围为 -2147483647~2147483647
      (初学者目前刷到的题目都是int就能解决的,只要注意题目中有关于2147483647的提示就直接用int,巧记Tips:我们学校的学号正好和此范围都是十位数)
    • long:长度为4,范围为 -2147483647L~2147483647L (注意末尾有L)
    • long long:长度为8,范围为 -9223372036854775807i64~9223372036854775807i64
      (在算法竞赛中,为了不让长度作为判题有误的限制,一般使用long long
    • unsigned int: 范围为0~4294967295,可以简写为unsigned
    关于 signed 或 unsigned

    C语⾔使⽤ signedunsigned 关键字修饰字符型和整型的。
    signed 关键字,表⽰该类型带有正负号,可以包含负值;
    unsigned 关键字,表⽰该类型不带有正负号,只能表⽰零和正整数。
    默认不带此关键字的话,省略的前缀为 signed
    整数变量声明为 unsigned 的好处是,同样⻓度的内存能够表⽰的最⼤整数值,增⼤了⼀倍

    floating-point 浮点型

    ps:到浮点型的话,范围数字很长,所以这里直接用十进制小数点位数来代表范围

    • float:长度为4,单精度,精确到小数点后6位,范围的末尾有f
    • double:长度为8,双精度,精确到小数点后15位。
    • long double:长度为8(msvs)或16(gcc),精确到小数点后15位,但实际范围比double更大。

    character 字符型

    char 字符类型,长度为1,字符型的常量一般用单引号包围'',这点尤其注意。
    如果是转义字符,比如 \n\0\t都算作一个字符。
    再比如有些看不见的操作也可以算作一个字符,比如空格和换行。

    bool 布尔型

    C语⾔原来并没有为布尔值单独设置⼀个类型,⽽是使⽤整数 0 表⽰false(假),⾮零值表示true(真)
    在 C99 标准中引⼊了布尔类型来表⽰真假的,需要包含<stdbool.h>头文件 #include <stdio.h>
    (对于老一点的编译器可能需要C99支持,比如CFree)
    布尔类型(_Bool)长度为1,其类型的变量取值仅有truefalse两个。

    #include <stdio.h>
    int main()
    {
        // 整型
        printf("%hd", 5); //short对应的
        printf("%d", 5);  // int对应的【常用】
        printf("%ld", 5);  // long对应的
        printf("%lld", 5);  // long long对应的
    
        // 浮点型:根据小数点后面取到的位数不同选择不同的数据类型
        printf("%f", 3.14f);  // float对应的,注意末尾有f
        printf("%lf", 3.1415); // double对应的 【常用】
        printf("%Lf", 3.1415); // long double 对应的,注意大写L
    
        // 字符型:字符和其对应的ASCII码值是对应的
        printf("%c", 'A');
        printf("%c", 65);
    
        // 字符串
        printf("%s", "HelloWorld");
    }

    输出格式

    基本格式

    printf() 不会在⾏尾⾃动添加换⾏符,运⾏结束后,光标就停留在输出结束的地⽅,不会⾃动换 ⾏
    那么其中有占位符表示输入格式的部分,需要借助双引号参数1外的参数加入这个位置。

    占位符到转换说明

    占位符就是写在第一个字符串参数中%开头的,形如%d%f,在一定位置可表示要填入的参数格式/控制参数输出格式的这类字符。
    占位符表格:

    数据类型变量声明类型占位符
    短整型short%hd
    整型int%d
    无符号整型unsigned int%u
    长整型long%ld
    无符号长整型unsigned long int%lu
    长长整型long long%lld
    单精度浮点型float%f
    双精度浮点型double%lf
    长双精度浮点型long double%Lf
    字符型char%c
    字符串型String%s
    指针(独立类型%p

    如果对于上面表格中写的部分,那么只看这种占位符其实也是转换说明,但如果加上下面这些后就算是转换说明,转换说明基本格式如同%m.pX 或 %-m.pX

    限定最小宽度

    在占位符中间插入数字(或转化说明基本格式中的小数点前的数字m)即代表输出的最小宽度。
    最小宽度的理解:宽度对于整型来说就是数字位数,如果说位数小于最小宽度,会在数字前面自动补空格直到是最小宽度;如果给出的位数已经超过了最小宽度,那么直接输出。
    默认在数字前面补空格即右对齐,如果需要左对齐的话,可以再在%后加一个-

    #include <stdio.h>
    int main(){
        printf("%d\n", 123);   // Output:"123"
        printf("%5d\n", 123);  // Output:"  123"
        printf("%-5d\n", 123); // Output: "123  "
        return 0;
    }
    始终显示正负号

    在占位符中间插入+就可以了。(和上面的的-的用法要分清哦!)

    #include <stdio.h>
    int main()
    {
        printf("%+d\n", 123);  // Output: "+123"
        printf("%+d\n", -123); // Output: "-123"
        return 0;
    }
    限定小数点后位数

    %和字母之间的小数点后紧接着的数字(或转化说明基本格式中的小数点前的数字p)就代表要保留的位数,会四舍五入到这个指定的位数。

    #include <stdio.h>
    int main()
    {
        printf("%.2f\n", 1.233);  // Output: "1.23"
        printf("%.2f\n", 1.236);  // Output: "1.24"
        return 0;
    }

    ps:默认不指定保留几位的时候,输出的最多位数取决于数据类型精度。
    可以和限定最小宽度的一起使用,小数点算一个宽度

    #include <stdio.h>
    int main()
    {
        printf("%5.2f\n", 1.236);  // Output: " 1.24"
        return 0;
    }
    输出部分字符串

    如果把上面限定小数点后位数的方法,用到字符串输出用的%s上,那就是可以指定字符串输出的长度了。

    #include <stdio.h>
    int main()
    {
        printf("%.7s\n", "Nibbles is handsome.");  // Output: "Nibbles"
        return 0;
    }
    限定值以 * 代
    #include <stdio.h>
    int main()
    {
        printf("%*.*f\n", 6, 2, 0.5);  // Output: "  0.50"
        return 0;
    }

    2. scanf() 输入

    除了按照上述的直接输出的程序方式。一个完整的简单程序应该有IO两部分(Process处理后续涉及):Input(输入),Output(输出)。 输出方式就是上面的 printf() 打印到屏幕,输入就是scanf()
    先给示例:

    #include <stdio.h>
    int main()
    {
        int num = 0;
        scanf("%d", &num);
        printf("Number: ", num);
    }

    变量

    定义 = 声明 + 内存分配

    int num = 0;

    这样是定义了一个变量 num,并将其初始化,初始值设置为0。
    变量的定义过程是 int num 部分,此时会在内存中分配一个空间。

    extern 单独声明

    extern int external_var;

    单独只有声明的需要使用 extern 关键字,一般在头文件中声明,但不分配内存。
    但是如果在其他文件中已经有定义了变量,在该文件中想要引用其他文件的定义可以使用 extern

    变量的生命周期控制

    全局变量

    在函数的 { } 之外定义的变量,初始化如果不指定是默认为0。
    如果是在定义之前使用 extern (如上在头文件中单独声明案例)
    在内存的静态存储区。在整个程序中:程序开始执行时创建,在程序结束时销毁。

    局部变量

    在函数的 { } 之内定义的变量,要是出了花括号的部分就没法再引用。
    在整个程序中:进入程序中变量所在函数时创建,在退出该函数时销毁。
    通常存储在栈区(auto 变量)。使用 static 关键字把变量变为静态变量,默认初始值才可以是0,否则没有初始值是随机数,此时在内存的静态存储区。

    变量的命名规则

    • 可以包含的字符:字母(a-z, A-Z)、数字(0-9)和下划线(_) 。
    • 第一个字符必须是字母或下划线。
    • 变量名区分大小写。
    • 变量名不能是C语言的关键字。
      • 关键字:auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, int, long, register, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while
    • 应选择具有描述性的变量名,以便于理解变量的用途(不建议学大学里课堂上那样,为了图快而使用abc这样的单字母变量名)。

    输入格式

    基本格式

    如下在程序执行时,会在冒号后面停留光标,等待你输入整数。之后回车即可把刚输入的整数赋给value整型变量。

    第一个参数就是格式化字符串,之后的参数的基本格式应该是 <取地址运算符><变量名>
    一次可以读入多个变量,但是要注意,有多少个占位符/转换说明,后面其后就要有多少个参数,遵循按顺序对应。如果一次输入多个值,各个值之间常可以用空格换行隔开,函数处理时候会⾃动过滤空⽩字符,包括空格、制表符、换⾏符。(但是如果是%c会把空格作为一个字符,如果要忽略可以写成% c,即在%和字母间加个空格)
    scanf()函数中的占位符决定了输入的格式。如果输入的格式不一样的话,会强制把输入的截断到与占位符表示的格式一样的部分。

    #include <stdio.h>
    int main()
    {
        int num_int;
        float num_float;
            // Input: "3.14"
        scanf("%d", &num_int);  
        printf("%d\n", num_int);   // Output: "3"
        scanf("%f", &num_float);
        printf("%f\n", num_float); // Output: "0.140000"
    }

    还可以简化为:

    #include <stdio.h>
    int main()
    {
        int num_int;
        float num_float;
            // Input: "3.14"
        scanf("%d%f", &num_int, &num_float);  
        printf("INT: %d\nFLOAT: %f\n", num_int, num_float); 
        /**
        Output:
            INT: 3
            FLOAT: 0.140000
        */
    }

    字符串输入不建议使用该函数,%s 不会包含空⽩字符,所以⽆法⽤来读取多个单词组成的短语/句子。读入时不会检测字符串是否超过数组⻓度。储存字符串时,很可能报错表示超过数组的边界。

    函数返回值和EOF文件结束

    在刷题时候,碰到题目要求函数式编程,在题目中已经给出了基本的代码结构,如下:

    #include <stdio.h>
    void print(int m, int n);
    
    int main() {
        int m, n;
        while (scanf("%d%d", &m, &n) != EOF) {
            print(m,n);
            printf("\n");
        }
        return 0;
    }

    在while的表达式里有关于scanf()函数返回值的条件语句。
    scanf()函数的返回值是个整数,且是和函数的占位符数是一致的。如果什么都没读取到就是返回0。如果在成功读取任何数据之前,发⽣了读取错误或者遇到读取到⽂件结尾,则返回常量 EOF (-1)。
    那么这个EOF的结束效果如何在我们输入中体现呢?
    Ctrl+Z 连按三次(VS2022中的效果),或者其他有的编译器中按一次即可。

    赋值忽略说明

    这一符号的设计主要是服务用户的。比如你规定了日期的输入格式为2025/4/19,结果输了个2025.4.19。此时就需要赋值忽略符(assignment suppression character)这样的处理了。

    #include <stdio.h>
    int main()
    {
        int year = 0;
        int month = 0;
        int day = 0;
        scanf("%d%*c%d%*c%d", &year, &month, &day);
        printf("%d-%d-%d", year, month, day);
        return 0;
    }

    这里的 %*c 中间的*就是作为赋值忽略符,这里的 %*c 可以不必和后面参数对应,不必返回,且读取到值也不输出,我们输出格式在 printf()函数里标定了 <year>-<month>-<day>

  • CTF Web 信息搜集 知识总结

    CTF Web 信息搜集 知识总结

    CTFer第一篇Web知识点汇总,从ctfhub和ctfshow部分题中总结,新手上路,请多指教。
    本篇内容仅供CTF学习使用!

    HTTP

    改参数重发请求,有带flag的Cookie需要URLdecode

    出题方向有Cookie欺骗、认证、伪造

    找Cookie可以通过浏览器自带的开发者工具也可以通过burpsuite

    开发者工具→Network→Cookies

    burpsuite通过设置好代理→截断,可以看到HTTP头的Cookie字段

    【HTTP 状态码】

    302跳转

    302:请求的资源临时移动到另一个位置。通常用于临时重定向,例如用户未登录时重定向到登录页面。

    其他状态码

    200:正常

    204:请求成功,但没有内容返回。用于不需要返回任何数据的情况下。

    301:请求的资源已被永久移动到新位置。

    400:服务器无法理解请求。可能参数错误或格式不正确。

    401:资源需要身份验证,客户端无权限访问。

    404:请求的资源不存在。

    500:服务器内部遇到意外情况,无法完成请求。

    503:服务不可用,或服务器暂时无法处理(过载或维护中)。

    【HTTP 标准方法】

    自定义方法探测

    还有标准方法:

    GET: 请求指定的资源并返回其内容。常用于获取数据。

    POST: 向指定资源提交数据,通常用于创建或更新资源。

    hackbar:“Load URL” → “Post data” → 输入要POST的字段

    burpsuite:见后文题目操作

    HEAD: 类似于GET,但只返回响应头,不返回实体内容。用于获取元信息。

    PUT: 更新指定资源的内容,通常用于替换现有资源。

    DELETE: 删除指定资源。

    OPTIONS: 查询服务器支持哪些请求方法,通常用于CORS(跨域资源共享)预检请求。

    TRACE: 追踪请求-响应链路,用于诊断目的。通常不推荐使用,因为可能会泄露信息。

    CONNECT: 建立一个隧道至目标服务器,常用于HTTPS代理。

    临时文件有关

    【Git】

    版本控制 .git下的主页文件 .svn/目录

    Git 是一个开源的分布式版本控制系统,主要用于管理项目的代码版本。

    主要由一个隐藏的 .git 目录控制

    文件

    HEAD:指向当前分支的最新提交,通常是一个指向 refs/heads/<branch-name> 的引用。

    config:存储当前仓库的配置选项,如远程仓库地址和用户信息。

    description:用于描述仓库,主要在 GitWeb 等工具中使用。

    COMMIT_EDITMSG:保存最近一次提交的信息,供用户参考。

    FETCH_HEAD:记录从远程仓库获取的最新提交信息。

    主页文件index.php index

    目录

    objects/:存放所有 Git 对象,包括提交(commit)、树(tree)、文件内容(blob)和标签(tag)。这些对象以 SHA-1 哈希值命名,确保唯一性。

    refs/:包含指向分支、标签和远程引用的指针。具体子目录包括:

    heads/:存储本地分支的最新提交。

    tags/:存储标签对应的提交。

    remotes/:保存远程分支的引用。

    logs/:记录所有引用(如分支和 HEAD)的历史变更,便于追踪操作记录。

    hooks/:存放钩子脚本,可以在特定事件发生时自动执行自定义操作,如提交前检查代码格式等。

    info/:存放一些额外的信息,如忽略模式(与 .gitignore 类似)。

    【PHP 相关文件类型】

    .phps 展示代码

    可以通过把*.php改为*.phps,从而下载到php文件的源码

    【vim】

    .swp 因非正常退出而保留的文件

    vim是一个强大的文本编辑器,广泛用于编程和系统管理。

    .swp 文件的特征

    隐藏文件:.swp 文件通常是隐藏的,使用 ls -a 命令可以查看。但是在生产环境中没有删除,也可以被下载到

    没有删除的原因:如果你正常退出 Vim(使用 :wq 或 Shift + ZZ),.swp 文件会被自动删除。如果没有正常退出,这个文件会保留在目录中。

    index,php后面加.swp

    【PHP 探针】

    phpinfo信息有flag的环境变量

    雅黑PHP探针:PHP探针内除了服务器的信息,还会有phpinfo,出题者在phpinfo中插入了flag的环境变量。

    网站默认配置问题

    后台没说就/admin一下

    【网站拨测】

    域名TXT记录

    工具网站:https://boce.aliyun.com/detect/dns

    DNS记录:

    A 记录:将域名映射到 IPv4 地址。

    AAAA 记录:将域名映射到 IPv6 地址。

    CNAME 记录:将一个域名指向另一个域名(别名)。

    MX 记录:指定邮件交换服务器,用于接收发送到该域的电子邮件。

    NS 记录:指定负责管理该域名的权威 DNS 服务器。

    SOA 记录:提供有关 DNS 区域的信息,包括主 DNS 服务器、管理员邮箱、序列号等。

    TXT 记录:用于存储任意文本信息,常用于验证和安全配置(如 SPF 和 DKIM)。

    PTR 记录:用于反向 DNS 查询,将 IP 地址映射回域名。

    CAA 记录:指定哪些证书颁发机构(CA)被允许为该域签发证书。

    管理员信息公布于网站或技术文档中

    密保问题的答案能公开查得

    【KindEditor】

    编辑器插入文件列出服务器文件列表

    KindEditor是一款开源的在线HTML富文本编辑器。

    文件空间显示服务器文件的原因:KindEditor中有一个选项允许用户浏览服务器上已上传的文件,这通常通过设置allowFileManager: true来实现(这还是程序的默认设置?!)。当启用此功能时,用户可以访问服务器上的文件管理器,从而查看和选择已上传的文件。

    解密相关

    【base64】

    编码decode

    【AES】

    加密过的密码,表单POST提交

    【unicode】

    编码decode得到隐藏页面提示

    解码网站

    (NSSCTF的在线工具,需要先登录):https://www.nssctf.cn/user/range/warjax/code

    数据库

    mdb文件(早期)

    不用管那些乱码小方格,直接搜flag就好

    sql文件