___ _ __ _ _ ___| |_
/ __| '__| | | / __| __|
| (__| | | |_| \__ \ |_
\___|_| \__,_|___/\__|
'crust' - Learn the basics of Rust and C side by side.
Here, I'll document my hurdles in learning Rust and utilise whatever knowledge I have in C. This repository must not be treated as a base for the upcoming publication of a top-selling ePub for the Rust programming language. As of today, 2022/10/14, the tutorial 'crust' is in its early stages. It is expected that 'crust' will undergo perennial refinements. I'll try to keep things as simple as possible so that anyone within the age group 10 to 15 can learn elementary-level Rust and C. Adults should also find the tutorial easier than the programming books recommended in their high school/pre-university curriculum. Things are expected to be short since I may have to look back. If possible, the learning sessions will be translated into the Bengali language. Auto-translation software will be used to translate the documents into Bengali. Grammar-checking services will be used occasionally in the English versions. Please create a pull request in the GIT branch 'rough' if you want to improve the learning experience or if you have a suggestion.
Your active participation might help hundreds of newcomers to learn the concept of coding. The flow of logic and algorithms are almost the same across programming languages. I want you to participate. I'm not interested in any indirect involvement.
Don't expect any long introduction, since I don't have time for that. I have to keep things short. Don't expect eBooks in ePub or HTML format at the moment. However, an auto-generated HTML version of the documents will be provided from time to time.
Please have a look at Tulu-C-IDE.
You will find instructions to set up the MSYS2 Build Environment in detail. Leave the Installation directory up to the MSYS2 installer (The default location is: C:\msys64
). For consistency, we won't be using the "Microsoft Visual Studio Build Tools" (a.k.a., Windows SDK) or the IDE version of the MSVC (a.k.a., Visual Studio). If you want to install MSVC, go ahead and do so. Examples provided here will compile either with MinGW (a variant of GCC that you can find in the MSYS2 repository) or MSVC. However, you're on your own if you choose MSVC; make sure you set up the Microsoft build environment properly through the Microsoft-provided vcvarsall.bat
.
I do not recommend a full-blown IDE like Visual Studio, QT Creator, KDevelop, Code::Blocks IDE, CodeLite IDE, or Eclipse CDT unless you have very specific requirements to run a particular IDE. You won't need any of them, at least for now.
To keep things simple and make the journey a hands-on experience, do not use the autocompletion feature in any of the editors mentioned. However, you are allowed to use those editors mentioned with their configurations (Tulu-C-IDE and Tulu-C-IDE Helix Edition). You can look at the autocompletion hints, but type the code yourself. In my opinion Tulu-C-IDE Helix Edition will be a better choice for learning. Type the codes yourself. Or else, you'll learn nothing. I mean, be a little honest with yourself. Don't cheat yourself.
The more you will type the code yourself, the less you'll forget what you've learned. If you are on MS Windows, try Geany or Notepad++. On Linux systems with GTK-dependent Desktop Environments, such as GNOME, XFCE, MATE, Cinamon etc., you can use Geany. If you are using a Linux distribution that ships a QT-based Desktop Environment, for example, KDE, LXQT etc., try Kate. Like Geany, Kate can be installed on MS Windows.
Kate comes with built-in LSP support. Thus, if Kate finds the two files .ccls
and compile_flags.txt
, you'll see autocompletion hints. So, beware and don't use autocompletion. Autocompletion is useful when you have some familiarity with the language, not at the time of learning. Nevertheless, Kate is also a text editor of choice among professionals and serious hobbyists for code editing. It works like a charm on my MS Windows 10 machine, my primary (work) computer.
Download the entire repository in a Zip archive. Extract the zipped file somewhere on your hard drive. Remember that the location (path to the directory where you want to extract the archive) must not contain any space or special character other than underscores (i.e., _
). Examples: G:\test_n_practice\crust
, /home/YOUR_USERNAME/crust/
.
Enter crust/
from the graphical file manager. R-Click inside the folder and find the option to open a terminal emulator there. Open two terminal emulators if you are on Linux, one for editing the source file with Helix and the other for compiling/executing the code. Type ls
(Linux) or dir
(Windows) to see the contents of the folder. You'll find runc.sh
that you can use on most Linux systems to build/run the code. Use runc.BAT
or runc-msys2x64.sh
on MS Windows machines. You don't have to open a separate console. Double-click on the DOSBATCH script and it will take care of the build process. Or, use the script runc-msys2x64.sh
with the "MSYS2 MinGW 64-bit" Bash Shell.
# Change the execution permission parameter of the script.
chmod +x runc.sh
# Run the script.
./runc.sh
Modify the files main.c
and main.rs
in crust/code/testbed/src/
along the way. You'll keep backups in a plain text file.
Find a file exercises.txt
. After learning a particular topic, write the code and notes to exercises.txt
on the topic you covered.
FAQ: Can I use the Power Shell? Yes, but it doesn't have any purpose here. Build chains are largely driven by makefile generators (CMake, Bakefile), make utilities (make, nmake, mingw32-make), and build environments (Cargo), regardless of the complexity of the project. Both theoretically and technically, any console interface can be used. Although, using the PS might involve unnecessary work sometimes. Look at this Stack Overflow thread. Instead, use an emulator like WezTerm and switch to the PS from there when required. For compiling C/C++/Rust projects, the more feature-rich, objected-oriented Power Shell language is not a requirement. The right tool for the right job matters the most without drifting away from the task at hand.
- Hello, World! The Skeleton and its Anatomy
- Receiving Inputs
- The Character Set
- Reserved Keywords
- Constants, Variables, and Keywords
- Data Types and Variables
- Data Types
- Variables
- Constants
- Characters and Strings
- Operators
- Rules for Constructing Instructions
- Decision Control
- Loops & Flow control
- Case Control
- A Brief Description of Pointers (C) and Slices (Rust)
- Functions and Pointers
- Tuple - A Rust-Specific Compound Data Type
- Array
- More on Arrays, Tuples, and Vectors
- Strings and Vectors
- The Preprocessor in C
- Ownership and Borrowing in Rust
- Ownership
- Borrowing
- Structures
- Unions
- Enums
- Modules
- Collections in Rust and The C Standard Library
- Error Handling in Rust
- Rust's Generic Data Types
- Console Input and Output
- Operation on Files (Input/Output)
- Type Inference and Type Casting
- Renaming Data Types and Typedefs
- Typecasting
- Bit Fields
- Pointers to Functions
- Functions Returning Pointers
- Functions with Variable Number of Arguments
- Unions
- Union of Structures
- Package Manager
- Iterator and Closure
- Smart Pointers
- Concurrency
- fork(), exec(), pthreads(), Multithreaded Programming
- Copyright Notice:
C:
#include <stdio.h> // Inclusion of header files.
#include <stdlib.h>
#include <string.h>
#include <math.h>
// This is how we write a single-line comment.
/* This line is also a single-line comment. */
/*
This is
a
multi-line
comment
*/
int main(int argc, char *argv[]) { // The compulsory main() function.
printf("Hello, C!\n"); /* printf(),
a function that outputs formatted strings to the console. */
} // End of the code block (here, the block is the function main()).
Rust:
// Notice that you don't need to include a header file.
fn main() { // main() is a compulsory function even in Rust.
println!("Hello, world!"); /* println!() is a macro
which is used to send formatted strings
to the console.
Notice that this line is also
a multi-line comment.
*/
} // End of the code block (here, the block is the function main()).
Comments are areas of code ignored by the compiler.
One who writes the code keeps pieces of information under comments as hints for what the code does in a specific section so that the logic of the code becomes easier to understand and stays maintainable in the future.
Comments in both C and Rust are enclosed within /* */
. Another style of writing a single-line comment is to write //
before writing the comment. // Single-line Comment
. Anything after the //
part is ignored by the compiler. Usually, // comments
are used as End Of The Line Comments or simply 'Line Comments'. We use //
where we don't need to write any code after the comment part. Whereas, /* */
can be used as inline comments, e.g.,
fn /* comment */ main() {
}
// Line comments which go to the end of the line.
/* Single-line/Multi-line/Inline comment */
fn main() { //main() is a compulsory function even in Rust (a comment indeed).
// ...
// ...
}
Multi-line Comments or Single-line/Multi-line/Inline/Universal Comments:
Comments enclosed within /* */
can be split across lines.
/*
This is
a
multi-line
comment
*/
Doc comments in Rust: Doc comments are parsed into HTML library documentation:
/// Generate library docs for the following section.
//! Generate library docs for the enclosing section.
We will restrict ourselves to 1) End Of The Line Comments, //
and 2) Multi-line/Universal Comments, /* */
.
A block of code or Code Block is a lexical structure of instructions grouped together comprising declarations, operators, and statements. Foxed?
Don't be! For now, remember that a code block is something grouped within brackets, e.g., {}
.
{ // an example of the start of a block of code
} // an example of the end of a block of code
Simply,
{
}
In our Rust Hello World example,
fn main () {
// ...
// ...
}
Here, the function main()
keeps its set of instructions within a block of second brackets {}
.
In C & Rust, main()
is the driving force of all programs. Every program that executes must have a main()
function that initialises the program execution.
All major programs you run on your machine are compiled from several source files. Each source file may contain thousands of lines of code. However, the function main()
initialises the program execution. main()
serves as an Entry Point to the program's startup.
The main()
appears only once, no matter how large the project is. In case you are isolating parts of the program into multiple shared libraries (*.dll or *.so), each shared library can be driven by its individual driving function DllMain()
. On the other hand, it is not mandatory to create a DLL Entry Point for creating a shared library. We will cover the creation and use of Shared Libraries and Static Libraries in C later.
#include <windows.h>
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved ) // reserved
{
// Example code for a DLL Entry Point function.
// Look here for more info:
// https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain
}
For now, remember that every program you write needs a main()
function.
#include <stdio.h>
includes a text file stdio.h
containing some routines, such as text input/output to the console. It (stdio.h
) is basically a plaintext source file. #include <>
is used to attach source codes. It is called the Include Directive in C. The Include Directive is also a macro. We will visit the section of macros later. We will have to revisit it quite a few times.
What does the file stdio.h
do here, should be our question at the moment. It contains routines for text input/output to the console and some other routines. Compiler writers supply pre-defined routines prescribed by the ISO C standardisation committee for common tasks. Libraries outside the Standard C Library are also used for special purposes, such as Graphics, image/video processing, Graphical User Interface, dealing with intricate mathematical problems, scientific/business application programming etc. In our Hello World example in C, we used the pre-defined function printf()
for text output to the console.
Notice that we didn't include any standard library header like stdio.h
in our Rust Hello World program.
Functions are called the building blocks of programs. A function contains a set of routines to accomplish a particular task. Think of a car assembly line where every section plays different roles, and those who work in each department in the assembly line take their own part, making each department a complete set but also an independent part of the entire workforce. Think of functions as departments in that assembly line. Functions can also be compared to Bricks that are used in construction works. Your room contains hundreds of bricks.
The function pow(x,y)
returns x
raised to the power of y
i.e. x^y
(x
to the power y
). pow()
is declared and defined in the C standard library math.h
.
We included stdio.h
to call the function printf()
. It is used here for sending text output to the console.
MACRO: The Search-n-Replace utility in the compilers.
To be precise, 'Macros' are a group of characters to be replaced by the compiler with another predefined set of characters at the stage known as 'Preprocessor'. Macros and Preprocessors will be discussed later.
#define ADD_TWO_NUM (a, b) (a) + (b)
#defile MAX_VALUE 1000
Macros in C always start with the symbol #
.
ADD_TWO_NUM(a, b) (a) + (b)
ADD_TWO_NUM(8, 3)
#include <stdio.h>
#define ADD_TWO_NUM(a, b) (a) + (b)
int main() {
int u = 2;
int v = 3;
printf("%d\n", ADD_TWO_NUM(u, v));
return 0;
}
gcc test.c -o test.exe
test.exe
5
How macros get expanded:
gcc -save-temps -c test.c
gcc -E test.c > test.i
test.i
# 5 "test.c"
int main() {
int u = 2;
int v = 3;
printf("%d\n", (u) + (v));
return 0;
}
Look how the macro ADD_TWO_NUM(a, b)
got expanded to (u) + (v)
. Somewhat similar to a search-n-replace utility!
The Four Stages of Compilation in C:
Compilation: Compile is a process of transforming source codes into machine language that computers speak. The process of compilation passes through four stages.
-
Preprocessor (Compiler-generated Intermediate C/C++ File,
test.c
->test.i
.) -
Compiler (Assembly Language Code,
test.i
->test.s
.) -
Assembler (Assembly Language Code to Relocatable Object Code,
test.s
->test.o
.) -
Linker (All Relocatable Object Code Files to Machine-Readable Native Executable Code,
test.o
->test.exe
.)
Compilers' primary job: A Compiler is a program/piece of software/utility/application that turns source codes into machine-native binary executable files.
Extra Features: Beside performing designated task of translating source codes into machine native executable files, compilers may provide other facilities such as error checking, detection of runtime error, memory leak detection, Language Server Protocol for autocompletion etc.
Stage 1 (Preprocessor):
gcc -E code.c > code.i
In this stage, the compiler toolchain performs the following tasks:
-
Comment stripping: The compiler toolchain strips all comments and replaces them with single spaces.
-
Header inclusion and producing text blobs: The compiler toolchain attaches (includes (
#include< >
)) the instructions written in the header files (*.h
) and creates a single blob of text. -
Macro expansion: The compiler toolchain expands predefined macros.
Here, in this stage, the compiler takes all source files and generates individual intermediate text blobs (*.i
). Those blobs contain routines declared in header files attached to your source codes using the include directive #include < >
.
Stage 2 (Compiler):
gcc -S code.c > code.s
In this stage, the compiler toolchain transforms the preprocessed text blobs (*.i
) into Assembly Language codes (*.s
).
Stage 3 (Assembler) :
gcc -S code.c > code.o
Now the toolchain translates the assembly language codes into relocatable object codes (test.o
) that are still needed to be resolved by the linker. Relocatable Object Code files are not human-readable.
Stage 4 (Linker):
gcc code.c
In this stage, a separate program (in GCC/MSVC) in the toolchain called "Linker" takes all the Relocatable Object Code (*.o
) files and semi-compiled routines (functions etc.) from externally linked static/dynamic libraries (GUI, Graphic, Image Processing, Compression, Cryptography etc.). The linker must be told to search for those semi-compiled external files from specific locations along with the names of the libraries to be linked. We will come to Static and Dynamic Libraries later.
A brief overview:
Preprocessor Stage: For example, if your project contains three source files one.c
, two.c
, three.c
, the compiler will first generate one.i
, two.i
, three.i
. Assembler Stage: The compilation process will now pass through the Assembler which will translate the raw C codes (intermediate files, *.i
) into Assembly Language code files. Namely, *.s
. Compiler Stage: Then, those intermediate files will be converted to relocatable object code files one.o
, two.o
, three.o
. Linker Stage: In the next stage, the compiler will search for object code files (semi-compiled) libraries you used in your project. By combining all relocatable object code files the compiler will produce a final executable file.
If you've set up your project to split the program into separate Shared Library files and executable files, then the final rendition will contain Shared Libraries and executable files.
Rust however works in a slightly different manner.
-
Invocation:
-
Lexing and Parsing:
-
High-Level Intermediate Representation (HIR) Lowering:
-
Mid-level Intermediate Representation (MIR) Lowering:
-
Code Generation:
[Citation needed.]
Invocation: First, the Rust compiler (rustc
) is invoked by Cargo or directly by the user. The toolchain processes command-line options for optimisations and other tasks, such as installing the program after completing the build process. In this stage, rustc
performs check
-only builds (rather than producing executable machine code) with the help of rustc_driver
.
Lexing and parsing: The raw Rust source text is analysed by a low-level lexer. The lexer turns the source code into a stream of atomic code units which are called tokens. rustc_parse
takes the charge of passing the stream of tokens through a higher-level lexer. Macros get expanded. A set of validations is checked by the StringReader Struct and turn strings into interned symbols (Not our business. It is a way of storing only one immutable copy of each distinct string value). The parser translates the stream of tokens from the lexer output into an Abstract Syntax Tree (AST). Some intermediate files are generated that can be found in the rustc_parse
directory. For example,
expr.rs
pat.rs
ty.rs
stmt.rs
High-Level Intermediate Representation (HIR) Lowering: The Rust compiler collects the AST. The AST is converted to a more compiler-friendly representation called High-Level Intermediate Representation (HIR). The process of translating AST to HIR is called "lowering". We will learn the types of variables in C and Rust. In short, a variable type is like a unknown quantity in regular mathematics, x,y,z,u,v,r etc., except, the variable type must be declared to inform the compiler beforehand. A variable can be of an integer type (1, 5, 99, 1567, etc.), floating value (2.05, 7.01, 39459.04567 etc.), or a string of characters (Abracadabra, Smiley emoji, Your Name, a, b, c, d etc.). Other variable types also exists. Now, the compiler uses the HIR to do type inference. Type interface is the process of automatic detection of the type of an expression. A few more tasks are performed, like trait solving, and type checking.
Mid-level Intermediate Representation (MIR) Lowering: The compiler now translates the HIR to Mid-level Intermediate Representation (MIR), used for borrow checking. Rust also constructs the THIR, which is used for pattern and exhaustiveness checking. Some optimisations are performed. In my limited understanding of the internal working principles of the Rust compiler, it is a one-step extra refinement of the HIR.
Code generation: A process known as codegen begins at this stage. It is the stage when higher-level representations of the source performed in the earlier stages are turned into a machine-native executable. rustc
converts the MIR to LLVM Intermediate Representation (LLVM IR). It is done by LLVM software. LLVM stands for Low Level Virtual Machine.
We don't have to understand most of the Rust compiler stages to understand Rust programming. It is how Rust works in the background. As long as we are able to build our project using Cargo, we won't pull open the bonnet.
Programming Languages and Compilers: Remember that Programming Languages are sets of rules defined by a committee, and Compilers are programs that follow their guidelines. Different compiler vendors can make different compilers as long as they follow the same guidelines. Much like "the Shops and Establishments Act" that empowers you to open a shop but you'll have to follow the law. You cannot do whatever you want in your shop because you run the shop. Every financial institution have to abide by the rules mentioned in "the Companies Act". Similarly, you can drive a car as long as you follow the traffic rules. One compiler may give you some advantages over others. However, the rules formed by the organisation that standardises the guidelines, a.k.a., the standardisation committee, must be followed by the compiler vendor.
Enough about macros and the working principles of compilers, let's come back to macros in Rust.
Here, in our Rust Hello World example, println!()
is a macro. Unlike printf()
in C, it is not a function.
Notice the NOT/ Exclamation mark (!
) after println
. In Rust, macros are denoted by an !
mark at the end of the macro before using it. We don't have to look under the bonnet to discover how Rust expands println
at the moment. What is crucial for us to know right now is the use of the println!()
macro for text output in the console in Rust. One important note, text input/output is called formatted string input/output in C and Rust.
More about macros later.
printf("%d\n", ADD_TWO_NUM(u, v));
println!("Hello, world!");
;
Every instruction/command in C/Rust is a Statement. The end of the statements must be denoted by a semicolon, ;
. A C/Rust function, a macro in Rust in a block of code, are examples of individual statements. Notice the use of semicolons in the upcoming chapters.
Let's Break Another Skeleton:
It will be easier to start with C.
Area of a Circle.
/*
A C program to calculate the area of a circle.
*/
// https://byjus.com/maths/area-of-circle/
#include <stdio.h>
#include <math.h>
#define PI 3.14 // 22/7 = 3.14 (approx.)
int main(void) {
float radius = 0;
float area = 0;
printf("Type the value for the radius of the circle and hit Enter:\n");
scanf("%f", &radius);
printf("Radius = %f\n", (double)radius);
area = (float)(PI * (pow((double)radius, 2))); // The formula: area = pi * r^2
printf("Area = %f\n", (double)area);
return 0;
}
We've already discussed the include directive. You can attach any source code in text format with an extension that the compiler recognises, e.g., #include <math.h>
. The files you attach contain some pre-defined routines that save you time. You can call a function without having to write it from scratch. We've also discussed that the Include Directive is a macro.
#define PI 3.14
is a Preprocessor Directive (macro) that replaces PI
with 3.14
wherever it finds PI
inside the code section.
int main(void) {
area = (float)(PI * (pow((double)radius, 2))); // The formula: area = pi * r^2
}
As we saw earlier, every program must have a main()
function that initiates the program. A function contains a series of instructions inside that function block. Besides that, a function can return a value or return nothing (void
). int main()
means that the main()
function returns an integer value upon completion, after going through all instructions. int main()
, In this case, the value has to be either ZERO (0
) or ONE (1
). If the main()
function returns Zero, that means the function has completed successfully, and encountered Zero errors. When a function is intended to return nothing, it is written as void function_name()
, or void *function_name()
, or void **function_name()
etc. However, ISO C Standard Committee specifies that the main()
function is not allowed to return a void
value. void
: It denotes a non-existing value. Don't write void main()
.
Is void main()
strictly forbidden and illegal? Yes. However, like all rules, there are exceptions. You'll find void main()
in codes written for embedded systems in many instances. Most microcontroller vendors also sell their own version of C compilers targeting that specific platform (controller ICs). They don't strictly follow that int main()
rule. The programs we run on our PCs, servers, and peripherals run on top of an operating system. In embedded systems, more often than not, there is no operating system to collect that return value from the main()
, even if the main()
function returns a value. Moreover, embedded systems are designed to run indefinitely unless a power interruption is encountered. So, it is crucial for the code that executes on those systems to be free from any runtime error and the main()
function doesn't have to stop itself anyway. A return value is purposeless in either of the cases. Nevertheless, it is a good practice to follow conventions and write int main()
in codes written for embedded systems.
Function parameters: A function can take values for performing a task. For example, double sqrt(double x)
is a standard C library function found in math.h
. What does this mean? A double is a data type used for storing high-precision floating-point numbers in memory registers. Everything we do on our computers involves memory operations. We will come to float and double later. The function sqrt()
returns a fractional number in the range of 1.7E-308
to 1.7E+308
, which is 8 bytes. E is a scientific notation that stands for Exponent of 10 (the power of 10). 2.54E16
means sqrt()
we see that it takes a value of the size of double. x is a variable that is used for holding the value in memory before passing it to the internal instructions of the function sqrt()
, that is, sqrt(double x)
. So, the function takes a fractional number (float/double) through sqrt(double x) and returns the result which is also a fractional number (float/double), double sqrt()
. How to use that function?
double hypotenuse = 0.0;
double side1 = 16.238;
double side2 = 20.552;
hypotenuse = sqrt((side1 * side1) + (side2 * side2));
printf("Hypotenuse: %lf\n", hypotenuse);
Pythagorean Theorem:
Another way to use the function sqrt()
:
double result = 0.0; // initialising the variable
double fixed_fractional_value = 100.00;
result = sqrt(fixed_fractional_value);
printf("Result: %lf\n", result);
Now it is clear that a function can take some value as arguments and return a value. An important note: A function is allowed to return only one value. Thus, a function can receive no value (void
) and return a value upon completion. A function (other than the main()
) can take arguments as pointers (we will visit a dedicated chapter on pointers), performs a task, and return the result alternatively via the received pointers without returning anything through a regular return parameter, void function_x(int *integer_value, char *a_string, float fractional_no)
.
So, the structure is:
return_parameter function(argument_one, argument_two, argument_three, argument_four, arg_so_on)
Or, in better words,
data_type function(data_type variable1, datatype variable2, data_type pointer, data_type_so_on_so_forth)
Or, in better words,
int/char/float/double fn(int var1, float *var2, double *var3, char **string)
Etc.
Our main function is allowed to either receive two parameters main(int argc, char *argv[])
or receive nothing main(void)
. It is allowed to leave the argument section blank, main()
. In case it is left blank, main()
will not receive any value. Now ask me what is the purpose of receiving arguments through the main()
function. What did you type in the console to obtain the assembly language output of your first code? gcc -S code.c > code.s
, right? gcc
, the compiler, is a program. -S
and code.c
are arguments. The console (e.g., CMD.EXE) you are using is a program,>
is the argument that tells it to redirect the output to a file code.s
(that too is an argument). In the first chapters, we will restrict ourselves to int main(void)
, so no worry!
float radius = 0;
float: Float is a datatype for storing fractional numbers. In C, a fractinal number is either a float or a variant of it, such as double. double means long float which can store bigger numbers than float
. These are called floating point numbers. float usually has a storage size of 4 bytes. It is a 32-bit IEEE 754
single precision value in the range of 1.2E-38
to 3.4E+38
with precision up to 6 decimal places. You may ask me about the purpose of so many data types. Nothing is unlimited. Our computers have a finite amount of memory, no matter how big it is. Then, there must be a way for the Assembler to determine the size and type of a variable to make the code able to work step by step internally, which is unrelated to the size of your computer memory.
Storage Class and Data Types will be discussed later. We'll deal with five data types, int
, variations of int
, float
, double
, char
primarily, although all Data Types will be covered.
Here, we will be using the variable radius
(a fractional number) to store the result of the calculation in our code to get an output. float radius
is the part that deals with variable declaration. First, we write the datatype (here, float
), then we give our variable a name, radius
.
There are rules for declaring variables which we will see in a dedicated chapter on variables. For now, remember that a variable name must not start with a Capital Letter, Number, or a Special Character other than an underscore. Only small letters and underscore are allowed to be placed in the beginning of a variable name. Special Characters and Blank Space cannot be used anywhere in any naming (variable, structure, function etc.) convention. Numbers and Capital Letters can be used after writing the variables' initial characters legitimately. We will come to it later.
Legal:
int variable
, int _variable
, float variable01
, float variable_01
, char stringVariableTwo
Illegal:
int Variable
, float 01variable
, int v@r!able
, float variable 01
Variable Initialisation: The compiler must have some idea of the value a variable is holding at any moment in the process of execution. If the compiler doesn't find a value, it will create one. The automatic variable initialisation creates a randomly generated value which is known as Garbage Value. A garbage value will produce unintended result. If the compiler doesn't create a value for an uninitialised variable, the code will try to access a memory location that doesn't exist. The program will crash, leading to unprecedented consequences. It is also a strict rule to initialise the variable immediately after declaring it (C & Rust), or initialise the variable before accessing it (C). By writing float radius = 0
, we initialise the variable immediately after the variable declaration.
Have some Fun: Semicolon: ;
Don't forget the use of semicolon (;
). In a lot of situations I tried to figure out what went wrong with the code and found that a missing semicolon was preventing the compiler from compiling the code. Finding such pesky omissions is finding a needle in a haystack. Nip those small silly oversights in the bud.
Assignment Operators: =
is an Assignment Operator that binds a value to a variable.
NOTE: It has very little to do with the =
means giving a variable a value; simply pouring some water into a glass. The sign =
is not used for comparison in C. We will discuss Operators in the relevant chapter.
printf("Type the value for the radius of the circle and hit Enter:\n");
printf()
is a standard library function (int printf(const char *format-string, argument-list)
) declared in the header file stdio.h
that takes two arguments in the format (const char *format-string, argument-list)
and returns an integer
value after completion.
const char *format-string
means the the first argument (before the comma that separates it from the second argument) takes a formatted string specified by the string conversion specifications in the C programming language.
A bit more on format specifiers:
Before we dive into examples, let us declare variables:
Know the number of bytes a datatype can store. Examples:
printf("Size of int is: %llu\n", sizeof(int));
printf("Size of unsigned int is: %llu\n", sizeof(unsigned int));
printf("Size of short is: %llu\n", sizeof(short));
printf("Size of char is: %llu\n", sizeof(char));
A bit can store 2
values (0
and 1
).
8
bit 1
byte. Conversely, 1
byte 8
bit.
An int
can store 4
bytes, or simply,
That means, 2 values (0
& 1
) multiplied by 32 (per byte)
When a variable is signed, the most significant bit is reserved for the sign itself reducing it to the total capacity minus one. 0
denotes a positive number, and 1
denotes a negative number, thus, making the room available for a signed int
in the range of -2^31
to 2^31-1
, and for the unsigned ones 0
to 2^32-1
. Do you have the Microsoft Calculator installed on your computer. Go to the Scientific Calculator Mode and find the value of 2^31
. It is 2147483648
. The range of a signed int
(or int
) is -2147483648
to 2147483647
. The range of an unsigned int
is 0
to 4294967295
. I use the MATE Calculator on my Ubuntu XFCE machine. On Ubuntu GNOME, you'll find the GNOME Calculator. Most modern calculators have a scientific mode.
int an_integer = 10;
float a_fractional_no = 11.3268;
float a_bigger_fractional_no = 11238879.2675468746768768713;
long int a_big_integer = 123669788445664446;
long int a_big_integer = 12366976;
or, simply long a_big_integer = 12366976;
long long int a_very_big_integer = 123669765464787971;
unsigned int a_non_neg_no = 42949672;
long unsigned int a_non_neg_big_no = 1844674405;
char a_sentence[26] = "Abracadabra in a sentence!";
or, char a_sentence[] = "Abracadabra in a sentence!";
double _expo = 5.2376e+02;
Try it yourself.
Template:
#include <stdio.h>
int main(void) {
long long int a_very_big_integer = 123669765464787971; // Initialised the variable
printf("Size of long long int is: %llu\n", sizeof(long long int));
printf("%lld", a_very_big_integer); /* Prints the initialised value */
return 0;
}
Output:
Size of long long int is: 8
123669765464787971
Format Specifier | Description | Data Type (unless Not Applicable) | Examples | Size (in Bytes) | Range (on a 64-bit compiler) | Precision |
---|---|---|---|---|---|---|
%% |
Prints the % sign itself. |
N/A | printf("%%"); Output: % |
|||
%d or %i |
Prints a signed integer. 10 , -3 etc. |
int |
printf("%d", an_integer); Output: 10 |
4 | -2147483648 to 2147483647 |
|
%ld |
Prints a long signed integer. | long int |
printf("%ld", a_big_integer); Output: 12366976 |
8 | -9223372036854775808 to 9223372036854775807 |
|
%lld |
Prints a long long signed integer. | long long int |
printf("%lld", a_very_big_integer); Output: 123669765464787971 |
8 | -9223372036854775808 to 9223372036854775807 |
|
%u |
Prints an unsigned (non-negative) number. | unsigned int |
printf("%u", a_non_neg_no); Output: 42949672 |
4 | 0 to 4294967295 |
|
%lu |
Prints an unsigned (non-negative) long integer number. | long unsigned int |
printf("%lu", a_non_neg_big_no); Output: 1844674405 |
8 | 0 to 18446744073709551615 |
|
%f |
Prints a mid-range floating-point (fractional) number. | float |
printf("%f", a_fractional_no); Output: 11.326800 |
4 | 1.2E-38 to 3.4E+38 |
Up to 6 decimal places. |
%lf |
Prints a double which can hold bigger values than float (also a floating-point [fractional] number). [double means long float .] |
double |
printf("%lf", a_bigger_fractional_no); Output: 11238879.000000 |
8 | 2.3E-308 to 1.7E+308 |
Up to 15 decimal places. |
%Lf |
long double |
16 | 3.4E-4932 to 1.1E+4932 |
Up to 19 decimal places. |
||
%c |
Prints a single character (an alphabet). 'A', 'm', 'W' etc. | char |
printf("%c", _one_alphabet); Output: W . |
1 | Signed: -128 to 127 . Unsigned: 0 to 255 . |
|
%s |
Prints a string of characters. "A Sentence." | char |
printf("%s", a_sentence); Output: Abracadabra in a sentence! |
1 byte per character | Depends on the size of the string length. | |
%e |
Prints an Exponential notation ( small 'e', 2.9738e+00 ) |
float or double |
printf("%e", _expo); Output: 5.237600e+02 |
|||
%E |
Prints an Exponential notation ( Capital 'E', 2.9738E+00 ) |
float or double |
||||
%g |
A more compact version of %e or %f . Insignificant zeros are omitted. |
float or double |
||||
%G |
Same as %g , with a Capital E . |
float or double |
Conversion Flags:
How many digits do you want to print, and how many decimal places do you want in the fractional values?
Flag | Instruction | Example | Description | Character |
---|---|---|---|---|
+ (Plus sign) |
Must print a sign character (+ or - ). |
%+3.4d |
1) The minimum number of digits to be printed (%3d ). 2) The number of digits to be printed after the decimal point (usually a Full Stop ASCII character). %3.4f will print fractions up to four decimal places. |
1) Field width. 2) Precision. |
- (Minus sign) |
Same as above, but Output the converted argument as Left-justified. | %-3.4d |
Do | Do |
0 (ZERO) |
Fill (called 'pad') with Zeros instead of spaces. | %03.4d |
Do | Do |
# (SHARP or HASH) |
Print an alternate form of the output. (Not our concern at the moment for learning C.) |
Escape sequence | Activity |
---|---|
\n |
Prints a New Line character. |
\t |
Prints a TAB character. |
\b |
Backspace (non-erase) |
\\ |
Prints a Backslash. |
\" |
Prints a Double-Quote, " . |
\' |
Prints a Single-Quote, ' . |
\f |
Form Feed/Clear The Screen. |
\a |
Bell Sound (plays a speaker beep). |
\r |
Carriage Return. (Enter). |
\v |
Vertical tab. |
\? |
Question mark. |
\xnn |
Hexadecimal character code nn (Not our concern). |
\onn |
Octal character code nn (Not our concern). |
\nn |
Octal character code nn (Not our concern). |
The Return Value of printf():
Upon successful completion, printf()
returns the number of bytes (int
) it printed.
How will you print "
using printf()
? Simple!
\"
#include <stdio.h>
int main(void) {
printf("Regardless of how intimidating\n");
printf("it seems in the beginning,\n");
printf("programming in \"C & Rust\" is\n"); // Notice the use of \" to print "
printf("a piece of cake in the end.\n");
return 0;
}
Regardless of how intimidating
it seems in the beginning,
programming in "C & Rust" is
a piece of cake in the end.
You printed "C & Rust"
using printf()
.
We've learned something about the printf()
function. Time to break down our code.
printf("Type the value for the radius of the circle and hit Enter:\n");
printf("")
is the minimum form of the printf()
function. You can write printf("");
in a line as a complete statement. Even then, the compiler will show you some warning messages like warning: zero-length gnu_printf format string [-Wformat-zero-length]
. That means anything you want to print to the console must be enclosed within a pair of double quotes, ""
. In C, a character/string is always surrounded by a pair of quotes, '
'
/"
"
.
The string Type the value for the radius of the circle and hit Enter:
is a string which must be enclosed within a pair of double quotes, like: "Type the value for the radius of the circle and hit Enter:"
. After printing the string, we want the cursor to be moved to the next line. \n
is an escape sequence that moves the cursor to the next line as we've seen in the table. Thus, the printf()
function prints the string and then places the cursor on the next line. Semicolon terminates the statement (a function. Here, printf()
).
scanf("%f", &radius);
The scanf()
function:
Purpose: The function scanf()
reads formatted (user) input from stdin
.
int scanf(const char *format, ...)
Here, the format
is a string that contains one or more type specifier(s) such as %d
, %f
, %c
, %Lf
etc.
...
means that the string may consist of a series of specifiers (more than one fixed-length string).
By now, you already know almost everything about format/type specifiers, %d
, %f
, %c
, %...
etc.
Some common usage of the function scanf()
:
int/float/char/... variable = initialised_value;
scanf("%d/%f/%c/%...", &variable);
One user-input:
int mangos = 0;
scanf("%d", &mangos); // scanf() to read the user input
printf("No. of mangos = %d\n", mangos);
Multiple input:
int mangos = 0;
char _one_ASCII_character = 'a';
float a_fractional_no = 0.0;
int rtrnd_from_scanf_ = 0;
printf("Type the no. of mangos <space> a single ASCII char <space> a fractional no. <Enter>\n");
rtrnd_from_scanf_ = scanf("%d %c %f", &mangos, &_one_ASCII_character, &a_fractional_no);
// scanf() to read multiple input
printf("no. of mangos: %d, ASCII char: %c, fractional no: %f\n", mangos, _one_ASCII_character, a_fractional_no);
printf("No. of items read: %d\n", rtrnd_from_scanf_);
Output (2nd. Multiple input):
Type the no. of mangos <space> a single ASCII char <space> a fractional no. <Enter>
7 Y 2.5
no. of mangos: 7, ASCII char: Y, fractional no: 2.500000
No. of items read: 3
Return value: On success, scanf()
returns the number of items (specified by the format specifier, %d
, %f
, etc.) it read. int
is the return type. In case of a read error, scanf()
returns a number
By the way, how will you force-kill a C program? CTRL+c.
What is &
?
The sign &
is called the Ampersand operator. It is known as the "Address Of The Variable Operator". For more info, look here. We will learn other applications of the Ampersand operator later.
[Image from Wikipedia.]
scanf()
reads user input from the stdin
. scanf()
needs to store the input somewhere in the memory. As a coder, it is your duty to instruct scanf()
to bind that input value to a variable. It's not magic. Computers don't understand variable names. Every variable has a definite memory address (memory registers) where the program stores the value related to that variable. Variables are names. They need a room to live in. The picture above shows an image of a Pigeonhole where every pigeon lives in a designated room (hole). Imagine a name for each pigeon. Imagine an address number for each hole, too. A variable is like a pigeon that lives in a box/hole that has an address. Each memory cell/register in your PC's memory can house only one pigeon (variable) at a time.
The fun part is that every pigeon (variable) in your computer can behave like a Matryoshka doll.
With exceptions that their physical size never shrinks. One Pigeon-Shaped Matryoshka Doll can house another Pigeon-Shaped Matryoshka Doll without changing their size, thus, essentially behaving like both a pigeon and a pigeonhole simultaneously.
Assume there are three variables, a
, b
, and c
. Of them, c
holds a user-supplied integer value, b
holds the address of c
, and finally, a
holds the address of b
.
a
points to b
, b
points to c
.
a -> b -> c.
c
has the actual value in store for them, all others are holding addresses.
Here's a rough 3D sketch (designed hurriedly) that demonstrates the fundamental principle of pointers and addresses.
Demo:
Think of variables as water and memory addresses as glasses. Pour c
's water into b
(glass), then b
's water into another glass a
.
Or imagine putting c
(glass) on top of b
(another glass), and b
on top of a
, stacking one on top of other glasses.
If you wish, you can also compare memory addresses as boxes. One exception, unlike real-life objects the size parameter doesn't change, and any of the boxes can hold other boxes. Find the glTF 3D file in 3d-models
.
#include <stdio.h>
/*
Install cdecl in MSYS2:
pacman -S cdecl
Add MSYS2 bin folders to PATH
Ubuntu:
sudo apt install cdecl
Use:
cdecl.exe or cdecl
Website: https://cdecl.org/
Help:
cdecl
?
*/
int main(void) {
int **a;
/*
explain int **a;
declare a as pointer to pointer to int
Here, 'a' points to an address which will hold the address of 'b'
*/
int *b;
/*
explain int *b;
declare b as pointer to int
'b' points to an address which will hold the address of 'c'
*/
int c = 3;
/*
explain int c;
declare c as int
'c' holds an int value
Specifically,
the variable 'c'-s address stores the actual integer data
*/
b = &c; // pour the address of c into b
a = &b; // pour the address of b into a
printf("%d\n", **a);
/*
print the value stored
at the final location
(c's address)
pointed to by 'a'
*/
return 0;
}
/*
Output:
3
*/
printf("addr of c %llu\n", &c);
printf("val at b %llu\n", b);
printf("addr of b %llu\n", &b);
printf("val at a %llu\n", a);
addr. of c 910962981260
val. at b 910962981260
addr. of b 910962981264
val. at a 910962981264
Read from bottom to top, outer box to inner box.
We will see it when we will discuss Pointers. For now, Pigeons, Pigeonholes, and Pigeons as Matryoshka Dolls are the easiest explanation of all I could explain at best.
Coming back to the Ampersand operator, we use this to point to the variables' addresses (memory locations/registers) where the program can store the values it received from the stdin
, using this &
operator as the value collector. The values of the variables get stored in their respective memory locations.
int var = 0;
scanf("%d", &var);
You can print the said address of the variable at any given moment of execution.
printf("%llu", &var);
Here's a complete overview:
#include <stdio.h>
int main(void) {
int var = 0;
printf("Type a number & hit Enter:\n");
scanf("%d", &var);
printf("The value of the var is %d\n", var);
printf("The address of the var (HEX):\n");
printf("%p", &var);
printf("\n");
printf("The address of the var (decimal):\n");
printf("%llu", &var);
printf("\n");
return 0;
}
Output:
Type a number & hit Enter:
7
The value of the var is 7
The address of the var (HEX):
0000001dd19ffcbc
The address of the var (decimal):
128070974652
We haven't left our Area of a Circle program yet.
Typecasting: It is a technique of converting one data type to another, e.g, float
to double
etc.
There are two kinds of typecasting: 1) Implicit and 2) Explicit.
When the compiler converts the datatype for calculating mixed types of variables, it is called Implicit Typecasting. Loss of precision may take place in the process since the compiler does so by following some pre-defined methods.
When the coder converts the type in the code, it is called Explicit Typecasting. Precision loss may occur if not done properly.
We declared our variable radius
as a float
, float radius = 0;
. To print it as a float
, we must convert its datatype.
The syntax for explicit typecasting is,
(datatype) expression
Thus,
printf("Radius = %f\n", (double)radius);
pow()
or double pow(double x, double y)
:
area = (float)(PI * (pow((double)radius, 2))); // The formula: area = pi * r^2
As we've discussed before, the function pow(x,y)
returns x
raised to the power of y
i.e. x^y
(x
to the power y
). pow()
is declared and defined in the C standard library math.h
.
pow()
expects double
s, not float
s. All our variables are float
variables. So, here we need typecasting to interfere. First we converted the radius
, then the outcome of the calculation (PI * (pow((double)radius, 2)))
.
Multiplication operator: It is a part of the Arithmetic Operators group, denoted by *
. The sign *
has other applications, such as indicating a variable as a pointer. For now, we will be using it as a Multiplication Operator, one of its many applications.
PI * (pow(...));
means multiply PI
by (pow(...))
. a * b
.
Now, We will be sending (printing) the total output to the console.
printf("Area = %f\n", (double)area);
Next:
return 0;
Upon successful completion, our C program will return
an integer value 0
to the operating system that indicates everything went as expected, no errors (0
errors) occurred.
The C version of the program to calculate the area of a circle is complete.
The Rust version.
/*
A Rust program to calculate the area of a circle.
*/
use std::io;
fn main() {
println!("Type the value for the radius of the circle and hit Enter:");
let mut user_submitted_radius = String::new();
let mut radius: f32 = 0.0;
let mut squired: f32 = 0.0;
let mut area: f32 = 0.0;
io::stdin().read_line(&mut user_submitted_radius)
.ok()
.expect("Couldn't read user input!");
radius = user_submitted_radius.trim().parse().expect("Invalid user input!n");
squired = radius * radius;
area = 3.14 * squired; // The formula: area = pi * r^2
println!("Area = {}", area);
}
Code Formatter in Rust (rustfmt):
How will you format the code for better readability?
rustfmt code.rs
Now we will see what the code does, line by line.
Notice that we didn't include a header file like stdio.h
. That doesn't mean Rust doesn't have a Standard Library concept like in C. We will call the Rust's Standard Library differently.
We will have to take user input and perform calculations before printing the result as output. To do that, we need to bring the io
input/output library into scope. The io
library is contained in the Standard Library, known as std:
.
The use
keyword -> Bring symbols into scope:
use std::io;
In C, we don't use a statement terminator ;
after a macro, #include <stdio.h>
. In Rust, we are calling a set of sub-routines io
into the scope from std
. It is a statement, so we are using the statement terminator. C and Rust are not exactly the same. So there will be some differences.
The double colon ::
Ref:
https://stackoverflow.com/questions/69756732/what-does-double-colon-mean-in-rust
Paths for Referring to an Item in the Module Tree - The Rust Programming Language
In the Stack Overflow thread as explained by the user "Netwave",
::
behaves like a namespace accessor. You can navigate through modules or specify locations likestd::io::stdin()
or call methods for objects like inString::new()
. It can even be mixed, since an object may be in a module itself, so for example, the full path to the String new method would bestd::string::String::new
.Refer here for more information.
In my limited understanding of Rust, it is used for accessing elements (specifically, functionalities and sub/routines) that are grouped together. Think of the serial assembly of individual links in a chain; to drag one individual link, you will have to tow preceding links.
We've already talked about The main() Function. fn
is a Rust keyword prefixed before declaring a function. Move on to the next line.
println!()
is a macro, which is used to send formatted strings to the console as we've discussed before. To send an unchangeable string to the console using println!()
, the string must be enclosed within double-quotes, "A String"
. println!("A String")
is the simplest example of its use.
The let
keyword in Rust is used to create/declare variables.
Keywords in C and Rust are reserved words. They cannot be used for naming variables/constants/functions/structures. Each keyword has its unique purpose, and its name is reserved for that specific purpose.
An example of let
:
let price = 2;
We declared a new variable named price
and initialised a value for it, 2
.
Mutable and Immutable Variables:
In Rust, variables are immutable by default. That means once we assign a value to the variable, the value won't change.
This is not very practical since the value may be changed during the program's run, or we may have to store the output in a variable to see the result of a calculation. After all, programming is more or less performing calculations faster.
To make a variable mutable, we add the mut
keyword before the variable's name:
let price = 2; // immutable
let mut dishes = 3; // mutable
The equal sign (=
) tells the compiler to assign a value to a variable. It is called the Assignment Operator. We will come to the Operators later.
let mut user_submitted_radius = String::new();
String
is a Datatype in Rust which can be classified into two categories: 1) String Object (String) and 2) String Literal (&str).
String Literal: By default, String Literals are static texts which always point to a fixed and valid UTF-8 sequence. The compiler knows the string at the compile time since it will not change during the program's run. The more technical terminology of String Literal (&str
) is "String Slices".
Some usages:
fn main() {
let ur_name:&str="Pinaki S. Gupta"; // Declaring a fixed string literal
let ur_d_o_b:&str = "1982/JUNE(06)/10"; // Declaring another fixed string literal
println!("Your Name: {}", ur_name); // Printing the fixed string
println!("Your Date of Birth: {}", ur_d_o_b);
}
Your Name: Pinaki S. Gupta
Your Date of Birth: 1982/JUNE(06)/10
String Object: Sting Objects are intended to be changed during the program's run. The Rust Standard Library provides the string input/output feature, defined in the standard library as a public structure, pub struct String. It (the String
object) is "growable", mutable, UTF-8 encoded, heap-allocated, and not null-terminated. It is used when the string value can be changed at the run time.
BTW, how to create a String Object?
String::new()
The following syntax creates an empty string.
String::from()
fn main() {
let blankstr = String::new();
println!("The empty str is: {}", blankstr);
let str01 = String::from("Rust is good!");
println!("str01 is: {}", str01);
}
The empty str is:
str01 is: Rust is good!
String Object calling Methods/Functions (More details).
Method/fn |
Signature | Description |
---|---|---|
new() |
pub const fn new() -> String |
Creates a new empty String. |
to_string() |
fn to_string(&self) -> String |
Converts the given value to a String. |
replace() |
pub fn replace<'a, P>(&'a self, from: P, to: &str) -> String |
Replaces all matches of a pattern with another string. |
as_str() |
pub fn as_str(&self) -> &str |
Extracts a string slice containing the entire string. |
push() |
pub fn push(&mut self, ch: char) |
Appends the given char to the end of a given String. |
push_str() |
pub fn push_str(&mut self, string: &str) |
Appends a given string slice onto the end of a given String. |
len() |
pub fn len(&self) -> usize |
Returns the length of a given String, in bytes. |
trim() |
pub fn trim(&self) -> &str |
Returns a string slice with leading and trailing whitespace removed. |
split_whitespa ce() |
pub fn split_whitespace(&self) -> SplitWhitespace |
Splits a string slice by whitespace and returns an iterator. |
split() |
pub fn split<'a, P>(&'a self, pat: P) -> Split<'a, P> , where P is pattern can be &str , char , or a closure that determines the split. |
Returns an iterator over substrings of this string slice, separated by characters matched by a pattern. |
chars() |
pub fn chars(&self) -> Chars |
Returns an iterator over the chars of a string slice. |
Examples of the Methods mentioned above:
new()
: new()
is used to create an empty string. Here, we create an empty string using new()
and appending the string Hi, Rust!
at the end of the empty string, thus, essentially initialising the variable.
fn main() {
let mut variab = String::new();
variab.push_str("Hi, Rust!");
println!("{}", variab);
}
Hi, Rust!
to_string()
: We convert a given variable value to a string using to_string()
. The text surrounded with double-quote ""
will be converted to a string object.
fn main() {
let a_string = "This is an example of to_string()".to_string();
println!("{}", a_string);
}
This is an example of to_string()
replace()
: Finds a pattern and replaces all matches with a supplied value. It is a function that takes two parameters, 1) A string pattern to search for, and 2) The new value which will replace all matches found. In our example, the pattern Rust
will be searched for and replaced with Crust
wherever found.
fn main() {
let some_str = "Rust isn't rusty!";
let another_str = some_str.replace("Rust", "Crust");
println!("{}", another_str);
}
Crust isn't rusty!
as_str()
: as_str()
extracts a string slice containing the entire string. We are finding and extracting the string slice C
which is contained in the variable a_word_str
.
fn main() {
let a_word_str = String::from("C");
let a_word_str_as_string = a_word_str.as_str();
println!("Example of as_str() {}", a_word_str_as_string);
}
Example of as_str() C
push()
: The push()
function appends a supplied char (here, s
) to the end of a String specified (Apple
).
fn main() {
let mut fruit = "Apple".to_string();
fruit.push('s');
println!("{}", fruit);
}
Apples
push_str()
: The push_str()
function/method appends a given string slice (here, is my self-help guide, not a tutorial.
) onto the end of a given String (here, Crust
). Here, the string to push is is my self-help guide, not a tutorial.
.
fn main() {
let mut paper = String::from("Crust ");
paper.push_str("is my self-help guide, not a tutorial.");
println!("{}", paper);
}
Crust is my self-help guide, not a tutorial.
len()
: Returns the total number of characters in a String (the length) in bytes (including spaces). We know that each character takes one byte in computer memory. So, the number of bytes returned is the number of characters found in a String.
fn main() {
let str01 = "C & Rust both are easy."; // 23 char -> C & Rust both are easy.
println!("Length: {}", str01.len());
}
Length: 23
trim()
: The function trim()
removes leading and trailing whitespace characters. NOTE: This function will not remove the inline spaces (whitespace characters found inside the string text). Only the whitespace chars found in the beginning and end will be trimmed.
fn main() {
let mut a_str_with_blank_spaces =
" Every programmer is an author. - Sercan Leylek \n";
println!("{}", a_str_with_blank_spaces);
println!("Length Before trim(): {}", a_str_with_blank_spaces.len());
println!(
"Length After trim(): {}",
a_str_with_blank_spaces.trim().len()
);
a_str_with_blank_spaces = a_str_with_blank_spaces.trim();
println!("{}", a_str_with_blank_spaces);
}
Every programmer is an author. - Sercan Leylek
Length Before trim(): 59
Length After trim(): 46
Every programmer is an author. - Sercan Leylek
split_whitespace()
: The function split_whitespace()
splits the whole input string into different words whenever a whitespace character is detected. The function also returns an iterator. We used the variable token
as the iterator to count the number of times the function detected a whitespace char.
Don't look at the for loop. We will learn Loops at the right moment.
fn main() {
let a_phrase = "Cato Dogo love Omelette Fish".to_string();
let mut i = 1;
for token in a_phrase.split_whitespace() {
println!("Word {}: {}", i, token);
i += 1;
}
}
Word 1: Cato
Word 2: Dogo
Word 3: love
Word 4: Omelette
Word 5: Fish
split()
: split()
finds a user-supplied pattern (here, ,
), then splits the String every time it detects a match. It also returns an iterator for counting. Note that the result cannot be stored for later use. There are workarounds such as collect()
, but it is beyond the scope of our skeleton anatomy. The workaround collect()
will be covered later.
fn main() {
let items = "Mango,Banana,Guava,Pineapple";
for token in items.split(",") {
println!("Item: {}", token);
}
}
Item: Mango
Item: Banana
Item: Guava
Item: Pineapple
chars()
: Returns an iterator over the chars of a string slice. That means each time the function char()
detects a character, it returns an iterator. So, we can count the number of chars in a Sting Slice and access individual characters. Although, we are not counting chars here.
fn main() {
let string01 = "Crust".to_string();
for i in string01.chars() {
println!("{}", i);
}
}
C
r
u
s
t
String Concatenation using the +
Operator and references (&
):
Appending a string to another string is termed String Concatenation or Interpolation. The result of this operation is a new string object. The +
operator internally calls the function add()
. The add()
function takes two parameters, 1) self
– The string object itself, and 2) The second parameter is a reference to the second string object (similar to the address of method in C).
// The add() function
add(self,&str) -> String {
// Returns a String object
}
We will be using syntax like let s4 = s1 + &s2 + &s3;
without looking at the internal mechanism.
fn main() {
let s1 = "C ".to_string();
let s2 = "and ".to_string();
let s3 = "Rust.".to_string();
let s4 = s1 + &s2 + &s3; // s2, s3 references are passed
println!("{}", s4);
}
C and Rust.
The format!()
Macro: It can also be used to concatenate strings.
fn main() {
let s1 = "C".to_string();
let s2 = "and".to_string();
let s3 = "Rust.".to_string();
let s4 = format!("{} {} {}", s1, s2, s3);
println!("{}", s4);
}
C and Rust.
Type Casting: Converting a number to a string and vice versa.
Integer to String:
fn main() {
let n = 714285;
let n_2_str = n.to_string();
// Num to str conversion
println!("The str is: {}", n_2_str);
println!("Operation successful: {}", n_2_str == "714285");
}
The str is: 714285
Operation successful: true
Integer to String and String to Integer (combined):
fn main() {
let n = 714285;
let n_2_str = n.to_string(); // Num to str conversion
let str_2_num = n_2_str.parse::<i32>().unwrap(); // Str to num conversion
// The parse() method can be used for converting strings to integers
// Procedure:
// let a_string = "25".to_string(); // `parse()` works with `&str` and `String`
// let an_int_val = a_string.parse::<i32>().unwrap();
// Num to str conversion
println!("Num to string: {}", n_2_str);
println!("Success: {}", n_2_str == "714285");
// Str to num conversion
println!("String to num: {}", str_2_num);
println!("Success: {}", str_2_num == 714285);
}
Num to string: 714285
Success: true
String to num: 714285
Success: true
By now, we know the purpose of the following line in our Area of a Circle program.
let mut user_submitted_radius = String::new();
Now we will decipher the next line:
let mut radius: f32 = 0.0;
let
is a keyword used to declare variables. The list of keywords in Rust can be found here, as we've discussed before.
We've also talked about mutable and immutable variables. By default, Rust variables are immutable, which means once you've assigned a value to a variable, it cannot be changed. To overcome this limitation, you'll have to declare/create variables as mutable variables. To make a variable Mutable, the mut
keyword is used. Values of mutable variables can be altered during the execution of the program.
We will discuss Variables and Data Types before trying to understand the purpose of the line let mut radius: f32 = 0.0;
.
Variables:
A variable is a named storage class which is used in C and Rust programming for storing numeric values or texts in the computer memory. Depending on the type of value a variable stores, a variable is closely associated with a particular Data Type. We've covered data types in C before. The data type determines two factors primarily: 1) The type of value (data) a variable stores (integer, character, fractional numbers etc.), and 2) The size (in bit) it will occupy in the computer memory.
Variable Naming Convention: Variable naming in Rust is quite similar to that of in C. We've covered variable naming in C. I'll repeat what we discussed, keeping things short.
Start with a small letter or an underscore (_
). After writing the first character of the variable, you can use numbers. There's an exception, unlike in C, you cannot use Capital Letters anywhere in Rust variable naming. So, don't use Capital Letters anywhere while giving a variable a name. Never use special characters or whitespace characters.
Legal:
variable
, _variable
, variable01
, variable_01
, variable_one
, string_variable_two
Illegal:
Variable
, 01variable
, v@r!able
, variable 01
, variableOne
, stringVariableTwo
The syntax for creating (declaring) a variable:
let variable_name = value; // DataType not specified
let variable_name:data_type = value; // DataType specified
Rust doesn't strictly enforce type declaration while creating a variable. The compiler infers the data type from the value assigned to the variable. However, you should specify the type for accessing the variables later with relative ease. Also, some errors can be avoided, and the compiler will produce better-optimised compiled code if you specify the type beforehand. The following are some examples of declaring fractional (float
) numbers. Note that Rust is a statically typed language, which means that it
must know the types of all variables at compile time. Either the compiler will do that, or the onus of doing so is left upon the person who writes the program.
let temperature: f64 = 27.092;
let mut radius: f32 = 0.0;
Primarily, Rust has two Data Types, 1) Scalar, and, 2) Compound. (Citation needed.)
Scalar Data Types:
-
Integers
-
Floating-point numbers
-
Booleans
-
Characters
Rust has two primary Compound Data Types:
-
Tuples
-
Arrays
Integer Types in Rust:
Length | Signed (+/-) | Unsigned (+) | How to declare |
---|---|---|---|
8 -bit |
i8 |
u8 |
let mut int_var: i8 = 0; or, let mut uint_var: u8 = 0; |
16 -bit |
i16 |
u16 |
let mut int_var: i16 = 0; or, let mut uint_var: u16 = 0; |
32 -bit |
i32 |
u32 |
let mut int_var: i32 = 0; or, let mut uint_var: u32 = 0; |
64 -bit |
i64 |
u64 |
let mut int_var: i64 = 0; or, let mut uint_var: u64 = 0; |
128 -bit |
i128 |
u128 |
let mut int_var: i128 = 0; or, let mut uint_var: u128 = 0; |
arch (architecture, 64/32/16/8-bit) | isize |
usize |
The isize
and usize
types depend on the architecture of the
computer the Rust program is running on. The datatypes isize
and usize
are primarily used for indexing some sort of collection.
What about the range? According to Rust's official documentation,
Each signed variant can store numbers from
$-\left( 2^{\left (n-1\right)} \right)$ to$+\left( \left( 2^{\left (n-1\right)} \right) -1 \right)$ inclusive, where$n$ is the number of bits that variant uses. So ani8
can store numbers from$-2^{8-1} = -2^{7} = -128$ to$2^{8-1} - 1 = 2^{7} - 1 = 128 - 1 = 127$ , which equals-128
to127
. Unsigned variants can store numbers from$0$ to$\left( 2^{n} - 1 \right)$ so au8
can store numbers from$0$ to$\left( 2^{8} - 1 \right)$ , which equals$0$ to$\left( 2^{8} - 1 \right) = 256 - 1 = 255$ .
Data Types and Ranges in Rust calculated using the same techniques we discovered when we calculated the range of each data types in C. Ultimately, the range of a data type begs one question, "how many bits a data type allows"? It doesn't matter whether you are calculating the range in C or Rust.
Number Separator: In Rust, you are allowed to use an underscore character (_
) as a separator while assigning numeric values to variables for better readability, such as 98_222
, which means 98222
. 5_000
equals 5000
in Rust. Thus, let mut var: i32 = 5_000;
is essentially the same as let mut var: i32 = 5000;
.
Literals:
fn main() {
// Suffixed literals, their types are known at initialization
let value = -257i64; // i64 (Length: 64 bit, signed.)
/*
Unsuffixed literals, their types are determined by
the Rust compiler depending on how they are used
*/
// let i = 13;
// let f = 1.6;
let x = value.abs(); // The function abs() returns the absolute value
println!("value is {}", value);
println!("The absolute value of x is {}", x);
// `size_of_val` returns the size of a variable in bytes (1 byte = 8 bits)
println!("size of `x` in bytes: {}", std::mem::size_of_val(&x));
println!("size of `x` in bits: {}", (8 * std::mem::size_of_val(&x)));
}
Here's a brief explanation from the Rust documentation:
std::mem::size_of_val
is a function, but called with its full path. Code can be split in logical units called modules. In this case, thesize_of_val
function is defined in themem
module, and themem
module is defined in thestd
crate.
Modules and Crates will be discussed later.
value is -257
The absolute value of x is 257
size of `x` in bytes: 8
size of `x` in bits: 64
Numeric literals (e.g., -52
) can be type-annotated (e.g., i32
) by adding the type (i32
) as a suffix (-52i32
). For example, to specify that the literal (integer number) 43
should have the type i64
, write 43i64
. For mutable variables, specify the type as let mut temperature: f64 = 0.0;
. For the immutable (fixed) ones, there's also the option to specify the type as let temperature: f64 = 27.092;
, as well as let temperature = 27.092f64;
. What if you initialise a variable like let mut value = 0i64;
? No problem. But, you'll have to use the old assigned value 0
before assigning a new value to the variable value
.
fn main() {
let mut value = 0i64;
value = -257;
println!("value is {}", value);
}
Compiler warning:
warning: value assigned to `value` is never read
--> testrst.rs:2:13
|
2 | let mut value = 0i64;
| ^^^^^
|
= note: `#[warn(unused_assignments)]` on by default
= help: maybe it is overwritten before being read?
warning: 1 warning emitted
value is -257
However, the following code will compile as usual.
fn main() {
let mut value = 0i64;
println!("value is {}", value);
value = -257;
println!("value is {}", value);
}
value is -257
The type of "unsuffixed" numeric literals will depend on how they are used. If no constraint exists, the compiler will use i32
for integers, and f64
for floating-point numbers.
Integer/Number Literals:
Integer/Number Literals | Can be expressed as: |
---|---|
Decimal | 90_000 |
Hexadecimal | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) |
b'A' |
Floating-Point Types in Rust:
Floating-point numbers are represented according to the IEEE-754 standard. The f32
type is a single-precision float, and f64
has double precision. The default type is f64
.
Length | Always Signed (+/-) |
---|---|
32 -bit |
f32 |
64 -bit |
f64 |
If unspecified, all fractional (float
) numbers default to f64
.
Basic Mathematical Operations:
-
Addition
-
Subtraction
-
Multiplication
-
Division
-
Remainder
Integer division rounds down to the nearest integer.
fn main() {
// addition // sum
let operation_addition = 15 + 3;
println!("operation_addition: {}", operation_addition);
// subtraction // difference
let operation_subtraction = 36.5 - 2.3;
println!("operation_subtraction: {}", operation_subtraction);
// multiplication // product
let operation_multiplication = 6 * 50;
println!("operation_multiplication: {}", operation_multiplication);
// division
let quotient = 87.4 / 49.3;
println!("quotient: {}", quotient);
let floored = 6 / 10; // Results in 0
println!("floored: {}", floored);
// remainder
let remainder = 27 % 5;
println!("remainder: {}", remainder);
}
/*
operation_addition: 18
operation_subtraction: 34.2
operation_multiplication: 300
quotient: 1.772819472616633
floored: 0
remainder: 2
*/
The Boolean Type:
The Boolean type in Rust has two possible values, either true
or false
. Booleans occupy one byte in size. A Boolean is expressed by the keyword bool
.
fn main() {
let yeah = true;
println!("yeah: {}", yeah);
let nope: bool = false; // with explicit type annotation
println!("nope: {}", nope);
}
/*
yeah: true
nope: false
*/
Boolean values are primarily used in Flow Control (which will be covered later) e.g., if
, else
, else if
, loop
, while
, for
to break or take another direction after meeting certain given conditions.
The Character DataType: The keyword char
is used to deal with Character Data Types.
fn main() {
let c = 'z';
println!("c: {}", c);
let z: char = 'ℤ'; // with explicit type annotation
println!("z: {}", z);
let thumbs_up = '👍';
println!("thumbs_up: {}", thumbs_up);
}
/*
c: z
z: ℤ
thumbs_up: 👍
*/
char
literals are specified with single quotes let c = 'z';
, as opposed to string
literals let name = "Pinaki";
, which are specified with double quotes. Rust’s char
type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than conventional ASCII characters. Unicode Scalar Values range from U+0000
to U+D7FF
and U+E000
to U+10FFFF
. See: Storing UTF-8 Encoded Text with Strings - The Rust Programming Language.
Compound DataTypes: Compound types can group multiple values into one type.
There are two kinds of Compound DataTypes in Rust,
-
Tuple
-
Array
The Tuple Type: Tuple is used to store values of multiple data types into one type, grouping them all together. It is created by writing a comma-separated list of values inside parentheses. Each position in the tuple has a type.
Demo: In the following program, we first create a tuple (a variable. Here, tup
) of values (237, 9.325, 7)
. We also specify the data types as (i32, f64, u8)
. Then we group three variables x
, y
, and z
using the keyword let
, and treat the group as the variable tup
itself, let (x, y, z) = tup
. Each variable inside the tuple-variable tup
is separated internally as x
, y
, and z
. The process is known as destructuring. It breaks the single tuple into three (equal to the number of values grouped together: 3) parts.
fn main() {
let tup: (i32, f64, u8) = (237, 9.325, 7);
let (x, y, z) = tup;
println!("The value of z is: {z}");
println!("The value of x is: {x}");
println!("The value of y is: {y}");
}
/*
The value of z is: 7
The value of x is: 237
The value of y is: 9.325
*/
We can also access a single element of a tuple directly by putting a dot/full stop (.
) after writing the index of the value we want to access. Note that the index starts from ZERO (0
), not ONE (1
). The first index in a tuple is 0
. Both in C and Rust, the counting of array elements starts from ZERO (0
). We will come to it in the chapter Array.
fn main() {
let tup: (i32, f64, u8) = (237, 9.325, 7);
let element_one = tup.0;
let element_two = tup.1;
let element_three = tup.2;
println!("element_one is: {element_one}");
println!("or, element_one is: {}", element_one);
println!("or, element_one is: {}", tup.0);
println!();
//
println!("element_two is: {element_two}");
println!("or, element_two is: {}", element_two);
println!("or, element_two is: {}", tup.1);
println!();
//
println!("element_three is: {element_three}");
println!("or, element_three is: {}", element_three);
println!("or, element_three is: {}", tup.2);
println!();
}
/*
element_one is: 237
or, element_one is: 237
or, element_one is: 237
element_two is: 9.325
or, element_two is: 9.325
or, element_two is: 9.325
element_three is: 7
or, element_three is: 7
or, element_three is: 7
*/
The Array Type: Array will also create a collection of multiple values. One notable exception, though: You cannot group values of a myriad of data types. Every element in an array must have the same data type. Also, arrays in Rust have a fixed length. In C, you can declare an empty array of unspecified length, and then change its length during the execution of the program using memory allocation techniques. We will see it in the chapter "Array".
The values in an array are grouped together by a comma-separated list inside square brackets.
fn main() {
let a = [1, 2, 3, 4, 5];
for i in 0..5 { // 0 to 4, plus one (extra)
println!("index {}: {}", i, a[i]);
}
}
/*
index 0: 1
index 1: 2
index 2: 3
index 3: 4
index 4: 5
*/
We could also declare the array in the above program as follows:
let a: [i32; 5] = [1, 2, 3, 4, 5];
// let arr: [datatype; no_of_elements] = [A, comma_separated, list, of, elements];
Arrays are useful when the number of elements does not change, for example, the days of the week.
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
For collections of changeable length, there is another Rust data type called Vector. A Vector is allowed to grow or shrink in size. Vectors will be discussed later.
Arrays can be initialised with the same values for all elements by specifying the initial value at the time of declaration.
let a = [5; 8]; // initial_value; length
/*
An array of 8 elements,
each of which
has the same initial value of 5.
*/
let arr = [initial_val; length];
So, let a = [5; 8];
is essentially the same as let a = [5, 5, 5, 5, 5, 5, 5, 5];
. We can initialise an array of 27
elements each of which will be of initial value 0
as let mut arr = [0; 27];
.
Accessing Array Elements: As we've seen before,
fn main() {
let a = [1, 2, 3, 4, 5];
for i in 0..5 { // 0 to 4, plus one (extra)
println!("index {}: {}", i, a[i]);
}
}
/*
index 0: 1
index 1: 2
index 2: 3
index 3: 4
index 4: 5
*/
Array elements can be accessed as a[i]
, i
being the
Or, Array elements can be accessed by assigning each element to a separate variable, and then, accessing the variable indirectly.
fn main() {
let a = [1, 2, 3, 4, 5];
let zeroth = a[0]; // assigning each element to a separate var
let first = a[1];
println!("index {}: {}", 0, zeroth); // accessing the var
println!("index {}: {}", 1, first);
}
/*
index 0: 1
index 1: 2
*/
Invalid Array Element Access:
Runtime panic at "Out of Bound Array Index". The "Index Out Of Bounds" error.
When a program tries to access an element of an array that is past the end of the array, an error will be generated during the execution of the program. The example shown below has an array variable a
, which contains 5 elements. Usually, the Rust compiler will flag an error message if your try to access the element that doesn't exist, for example, the 6th element. You can write a tricky program that fools the Rust compiler so the compiler fails to detect such a possibility at the compile time. Nevertheless, the error will be generated during the program's run, provided the program encounters such an invalid attempt to access a non-existent element. You cannot fool the Laws of Physics. Thus, you cannot move beyond a computer's limitations.
You don't have to understand every line of code in the following program at the moment. Compile and run it. Type 0 1 2 3 4 5 6 7
, that means you're telling the program to show the 0th, 1st, 2nd, 3rd, 4th, ..., and 7th elements consecutively.
See what happens when you run past the last index of an array. See it in action.
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read the line.");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number.");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
/*
0 1 2 3 4 5 6 7
thread 'main' panicked at 'Index entered was not a number: ParseIntError { kind: InvalidDigit }', testrst.rs:15:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
*/
fn main() {
let a = [1, 2, 3, 4, 5];
for i in 0..8 {
println!("The value of the element at index {} is {}:", i, a[i]);
}
}
/*
The value of the element at index 0 is 1:
The value of the element at index 1 is 2:
The value of the element at index 2 is 3:
The value of the element at index 3 is 4:
The value of the element at index 4 is 5:
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', testrst.rs:5:68
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
*/
Constants: We've already seen that variables in Rust are immutable (unchangeable) by default unless we use the keyword mut
to make them mutable. Values stored in immutable variables are not intended to change during execution. Like immutable variables, constants are values that are bound to names (like variable names) and are not allowed to change, although there are a few differences between constants and variables. The keyword mut
cannot be used with constants. Constants are not allowed to change under any circumstances.
Constants are declared using the keyword const
, unlike variables that are declared using the keyword let
. The type of the value must be annotated while declaring constants. The last difference is that expressions set to constants must consist of fixed values, which implies, constants cannot take values that are obtained after performing a calculation during the execution. 5 * 9
is allowed. However, let mut var: u32 = 2;
let mut var2: u32 = 3;
let mut result: u32 = 0;
some calculation;
result = var + var2;
const A_CONST: u32 = 25 * var + result;
won't be allowed.
/* An example of a constant declaration: */
fn main() {
const WEEKS_IN_A_YEAR: u32 = 365 / 7;
/* One calendar common year has 365 days */
/* 7 days per week */
println!("One year = {} weeks.", WEEKS_IN_A_YEAR);
}
/*
One year = 52 weeks.
*/
A Constant is not allowed to take any changeable value in its expression.
fn main() {
let mut var: u32 = 2;
let mut var2: u32 = 3;
let mut result: u32 = 0;
/* some calculation; */
result = var + var2;
const A_CONST: u32 = 25 * var + result; // an expression containing changeable values
println!("A_CONST = {}", A_CONST);
}
/*
Compiler error:
--> testrst.rs:7:31
|
7 | const A_CONST: u32 = 25 * var + result;
| ------------- ^^^ non-constant value
| |
| help: consider using `let` instead of `const`: `let A_CONST`
--> testrst.rs:7:37
|
7 | const A_CONST: u32 = 25 * var + result;
| ------------- ^^^^^^ non-constant value
error: aborting due to 2 previous errors
*/
However, constants can be brought into expressions/calculations normally. You can use constants without trying to alter their values; that is the purpose of constants.
fn main() {
/* 2 * pi * radius is the circumference of a circle */
const TWO: f32 = 2.0; // a constant
const PI: f32 = 3.142857143; // also a constant
let mut radius: f32 = 0.0; // a mutable var
let mut circumference: f32 = 0.0; // another mutable var
radius = 4.5; // assigning a value
circumference = TWO * PI * radius; // performing a calc
println!("Circumference = {}", circumference); // printing/using the result
}
/*
Circumference = 28.285713
*/
Shadowing: Shadowing is when you declare a new variable with the same name as a previously declared variable. Shadowing == Redeclaring. Think of it like x
is re-declared twice or more. It is defined as "The first variable being shadowed by the second". What does that mean? The second variable is what the compiler will see when you use the same name as the previously declared variable. The last declared one will be taken into consideration by the compiler. In effect, the second variable overshadows the first. When some other variable with the same name overpowers the previous one giving it the same treatment, the last one will be taken into account.
We can shadow a variable by using the same variable’s name by declaring it over and again, repeating the let
keyword as follows:
fn main() {
println!("The value of x...");
let x: u32 = 7; // The 1st scope.
// This program first binds x to a value of 7.
println!("...in the 1st scope is: {x}");
let x: u32 = x + 3; // The 2nd scope.
// Then it creates a new variable x again
// (notice the use of the keyword let).
// It takes the original value of x (7) and adds
// 3, so the value of x is 10.
println!("...in the 2nd scope is: {}", x);
{
// INNER SHADOWING with second brackets:
// { let repeat_var: datatype = new_value;}.
let x: u32 = x * 5; // The 3rd (inner) scope.
// The third let statement also shadows x and
// creates a new variable, multiplying
// the previous value by 5 to give x
// a value of 50.
println!("...in the 3rd (inner) scope is: {x}");
} // INNER SHADOWING {the second bracket} ends here.
// When that inner scope is over,
// the inner shadowing ends
// and x returns to being 10.
println!("...in the end is: {x}");
}
/*
The value of x...
...in the 1st scope is: 7
...in the 2nd scope is: 10
...in the 3rd (inner) scope is: 50
...in the end is: 10
*/
Shadowing is different from marking a variable mut
. One notable difference between shadowed variables and mutable variables is that we are not allowed to re-declare a mutable variable twice. See what happens if we decide to do so:
fn main() {
println!("The value of x...");
let mut x: u32 = 7; // The 1st scope.
// This program first binds x to a value of 7.
println!("...in the 1st scope is: {x}");
let mut x: u32 = x + 3; // The 2nd scope.
// Then it creates a new variable x again
// (notice the use of the keyword let).
// It takes the original value of x (7) and adds
// 3, so the value of x is 10.
println!("...in the 2nd scope is: {}", x);
{
// INNER SHADOWING with second brackets:
// { let repeat_var: datatype = new_value;}.
let mut x: u32 = x * 5; // The 3rd (inner) scope.
// The third let statement also shadows x and
// creates a new variable, multiplying
// the previous value by 5 to give x
// a value of 50.
println!("...in the 3rd (inner) scope is: {x}");
} // INNER SHADOWING {the second bracket} ends here.
// When that inner scope is over,
// the inner shadowing ends
// and x returns to being 10.
println!("...in the end is: {x}");
}
/*
warning: variable does not need to be mutable
--> testrst.rs:4:9
|
4 | let mut x: u32 = 7; // The 1st scope.
| ----^
| |
| help: remove this `mut`
* * *
*
*
...
... So many warnings here.
...
*
*
warning: 3 warnings emitted
*/
The other difference between mut
and shadowing is that a shadowed variable may have different data types at different points. In the following program, the variable spaces
takes the number of whitespace characters, and then we want to store that input as a number (how many spaces
) by shadowing the same variable:
fn main() {
let spaces = " "; // The first `spaces` variable is a string type.
print!("The str is: {},", spaces);
let spaces = spaces.len(); // The re-declared one is of a number type.
println!(" and the num is: {}", spaces);
}
/*
The str is: , and the num is: 3
*/
What is the advantage? Here, we can re-declare spaces
without having to come up with different names, such as spaces_str
and spaces_num
, which makes the job easier sometimes. Remember that shadowed variables cannot be mutable (mut
).
fn main() {
let mut spaces = " "; // The first `spaces` variable is a string type.
print!("The str is: {},", spaces);
let spaces = spaces.len(); // The second one is of a number type.
println!(" and the num is: {}", spaces);
}
/*
warning: variable does not need to be mutable
--> testrst.rs:2:9
|
2 | let mut spaces = " "; // The first `spaces` variable is a string type.
| ----^^^^^^
| |
| help: remove this `mut`
* ... warnings go on...
*/
By now, we know the meaning of the line:
let mut radius: f32 = 0.0;
In our Area of a Circle program.
We haven't left our "Area of a Circle" program yet.
Move on to the next lines.
let mut squired: f32 = 0.0;
let mut area: f32 = 0.0;
Rust Variables and Data Types in a Nutshell:
Do we need more explanation? You already know that. Let's look at this section of our Area of a Circle calculation program.
io::stdin().read_line(&mut user_submitted_radius)
.ok()
.expect("Couldn't read user input!");
If we go back to the beginning of the program, we will see that we created an empty string object by let mut user_submitted_radius = String::new();
. We learned that the double colon ::
is used to access individual elements (functions etc.) in a grouped module, similar to the serial assembly of individual links in a chain. We know that new()
is a function. We discussed most of the String Object calling Methods (Functions). The line let mut user_submitted_radius = String::new();
has created a mutable variable (user_submitted_radius
) that is currently bound to a new empty instance of a String
.
To take user input and perform calculations before printing the result as output, we brought the io
input/output library into the scope. The io
library is contained in the Standard Library, known as std:
.
The use
keyword -> Bring symbols into scope:
use std::io;
Here,
io::stdin().read_line(&mut user_submitted_radius)
.ok()
.expect("Couldn't read user input!");
We are calling the stdin()
function from the io
module, which will allow us to handle user input:
read_line(&mut user_submitted_radius)
Had we not imported the library std::io
into the scope, we could still access the function (method) read_line()
by writing std::io::stdin().read_line()
. Notice the chain links.
According to Rust's documentation:
The
stdin()
function returns an instance ofstd::io::Stdin
, which is a type that represents a handle to the standard input for your terminal.
The function read_line(&mut user_submitted_radius)
calls the read_line
method/function on the standard input handle to get input from the user.
BTW, what does that DOT sign (.
) do?
Look at the B - Operators and Symbols - The Rust Programming Language. One of the many uses of the The Dot Operator is that it can act like a dereferencing operator used to dig the method (fn) down from the dereference "tree", which happens at the compile time. There is no runtime performance penalty for finding methods (functions) and fields using the dereferencing techniques. DOT will perform auto-referencing, auto-dereferencing, and coercion until types match.
To be precise,
The
.
operator is used in accessing fields and methods of a reference. The.
operator automatically digs down deeper and dereferences a sequence of references.
Think of it as another way of pulling an individual link in a chain.
We’re also passing &mut user_submitted_radius
as the argument to read_line()
. The full job of read_line()
is to take whatever the user types into the standard input stream and append that input into a string (without overwriting its contents). We pass the string user_submitted_radius
as an argument. Look back. We declared that variable as let mut user_submitted_radius = String::new();
. Since the string was empty, only the user submitted value will be the final after appending the input to the empty string.
The string argument needs to be mutable so the method can append the user input to the string’s content.
The &
operator indicates that this argument is a reference to a mutable variable.
From Rust doc:
It gives you a way to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times.
The &
operator is very similar to the "address of" operator (&
) in C. Sort of Pointers? Sort of! References are a complex topic. For now, all you need to know is that like variables, references are immutable by default. Hence, you need to write &mut user_submitted_radius
rather than &user_submitted_radius
to make it mutable.
The Result
Type and handling failure:
The ok() method and The expect() method: read_line()
returns a value Result
after accomplishing the designated task, either an Ok
if nothing goes wrong, or an Err
. The Result
's variants are Ok
and Err
. Can this even happen? Imagine a situation where there's no console and no user to submit input. The method/function ok()
converts the value Result into a value Option (indicating how many bytes have been read), and the function expect()
gives either that Ok
value or shows an Err
message when it encounters an error. Panic in Rust is a circumstance when an irrecoverable error occurs. The function expect()
lets us detect where it occurs, in case of a panic. You must call this expect()
function. If you do not use the Result
value returned from read_line()
, the program will compile with serious warnings, and you will never know what happened in case of a bad event. Keep in mind that the function read_line()
eats whatever the user throws into the string passed out of the standard input (here, stdin()
), but it also returns a Result
value for you to deal with panics. See Error Handling in Rust, Module std::error
for details. Thus, the functions expect()
and unwrap()
are contained in the Standard Library std:
. To know more about the ok()
function, see Module std::result
. So, the function ok()
is found in the Standard Library std:
.
.expect("Couldn't read user input!");
Rust allows us to split codes across multiple lines as long we maintain proper Rust syntax, and so does C. Now we come to the third line of the same statement, which could also be written as: io::stdin().read_line(&mut user_submitted_radius).ok().expect("Couldn't read user input!");
in a single line.
There's no C-like short form of scanf()
. So, your programs are more memory safe from the ground up. Although, C programs can be equally or more memory safe when extra steps are not reluctantly avoided. Rust enforces safety from the start. After all, C is all about ultimate flexibility which is the power of C, not a drawback. However, in production-grade codes, the vast majority of the C Standard Library functions are avoided and substituted by specific implementations for a specific piece of code. It is quite natural that institutions like The Indian Space Research Organisation (ISRO), The Defence Research and Development Organisation (DRDO), The Bhabha Atomic Research Centre (BARC), GNU, Microsoft, The Linux Kernel Organization, and other influential entities won't accept code strewed with functions like scanf()
, strcpy()
, gets()
and such. Unpredictable behaviour and vulnerability are not the perfect options for any of them. They prefer writing their own for a specific purpose. Usually, people with limited resources writing codes under severe time constraints often take shorter routes. For teaching the language, books and websites use those function calls. You'll get a fair idea if you visit the following links:
c - What can I use for input conversion instead of scanf? - Stack Overflow
c - Implementing an alternative to scanf - Code Review Stack Exchange
Is it bad to use system command in C (code blocks compiler)? - Quora
Why system() is evil - C++ Articles
Why should the system() function be avoided in C and C++? - Stack Overflow
wiki.sei.cmu.edu/confluence|ENV33-C. Do not call system()
For example, instead of using system("clear");
we can use regex like:
/*
A program to clear the console by using
printf("\033[1;1H\033[2J");
*/
#include <stdio.h>
int main(void) {
int a;
printf("Type out a number:\n");
fflush(stdin);
scanf("%d", &a);
printf("\033[1;1H\033[2J"); // clears the screen [regex]
/*
https://www.geeksforgeeks.org/clear-console-c-language/
https://github.com/p-ranav/indicators/issues/62
*/
printf("You typed: %d\n", a);
return 0;
}
The scanf()
part is not secure in the program above. If you submit an integer variable greater than 2147483647
(namely, any extra beyond a variable that can hold 4
bytes), you'll notice strange output.
Similarly, methods in Rust like expect()
and unwrap()
are often avoided in production-grade codes.
Next:
radius = user_submitted_radius.trim().parse().expect("Invalid user input!n");
We discussed trim()
before.
trim()
: The function trim()
removes leading and trailing whitespace characters. NOTE: This function will not remove the inline spaces (whitespace characters found inside the string text). Only the whitespace chars found at the beginning and end will be trimmed.
The trim()
function is required here because you'll hit Enter after typing the input. Enter throws in a newline character \n
. That newline character has to be trimmed since the variable result
can only take 32-bit fractional (float
) numbers (f32
), not anything else.
parse(): The parse
method on strings converts a string to another type. It is here for converting a string to a number. We need to specify the data type we want to convert using the function parse()
. We declared the variable radius
as let mut radius: f32 = 0.0;
. Also, our user input is a string, let mut user_submitted_radius = String::new();
. Essentially the conversion goes from a string (input) to a floating point variable (radius
). To cut the long story short, parse()
is used in data type conversion.
The parse()
method will only work on characters that can logically be converted into numbers. What if the user input string contains A👍%
? So, some error checking must be performed.
expect()
: Have we not discussed The ok() method and The expect() method? Recap: The function expect()
gives either an Ok
value or shows an Err
message when it encounters an error. It lets us detect where an error has occurred, in case of a panic. You must call this expect()
function to identify unforeseen errors. Rust will also deduct whether the converted value assigned to the variable radius
is a 32-bit float
(i.e., f32
) or not. If parse()
fails, it will return the Err
variant of Result
which is a number to the computer, not exactly Ok
or Err
. The Err
will be collected by expect()
. If everything goes as expected, parse()
will return the Ok
variant of Result
, and from there on, expect()
will allow the rest of the program to carry on execution. See Error Handling in Rust, Module std::error
for details. Thus, the functions expect()
and unwrap()
are contained in the Standard Library std:
. To know more about the ok()
function, see Module std::result
. So, the function ok()
is found in the Standard Library std:
.
Read the following line forwards:
-
user_submitted_radius
will be filtered bytrim()
. -
parse()
will convert the input string to afloat
. -
If any error occurs,
expect()
will show us where it took place.
radius = user_submitted_radius.trim().parse().expect("Invalid user input!n");
Chain links? Don't you think so? Now you know what it looks like.
Move forward. Look at the next line.
squired = radius * radius;
Nothing's special. We're multiplying radius
with radius
, *
is an arithmetic operator, called the Multiplication Operator.
area = 3.14 * squired; // The formula: area = pi * r^2
Now, we're multiplying squired
by 3.14
. The resulting value will be assigned to the variable area
.
println!("Area = {}", area);
As we've seen, println!()
is a macro that we use while producing an output to the console.
println!()
Placeholders: In Rust, the {}
set of second brackets is a placeholder: think of {}
as a room that reserves the space for a value to be placed.
println!("");
is the minimum required syntax that you are allowed to write. Within the double quotes (""
), you can write a string of characters which will be printed to the console: e.g., println!("A Rust String.");
. To print the results of calculations, you either write the variables directly inside the pair of second brackets like println!("var01 = {var01}");
or put a placeholder (an empty pair of second brackets, i.e., {}
), and then write the variable names outside the double quotes (""
), such as println!("var01 = {}", var01);
. Note the use of the comma (,
) after the double quotes. If you put empty placeholders, variables are separated by commas. Unlike C, Rust doesn't need a format specifier to print values. In C, you need something like printf("var01 = %d", var01);
, and you cannot write the variables directly inside placeholders as you do in Rust, println!("var01 = {var01}");
.
Look at the program below.
fn main() {
let var01: u16 = 125;
let var02 = "Rust";
println!("var01 = {var01}");
println!("var01 = {}", var01);
println!("var02 = {var02}");
println!("var02 = {}", var02);
println!("var01 = {var01}, and var02 = {var02}");
println!("var01 = {}, and var02 = {}", var01, var02);
}
/*
var01 = 125
var01 = 125
var02 = Rust
var02 = Rust
var01 = 125, and var02 = Rust
var01 = 125, and var02 = Rust
*/
By now, we've successfully taken apart our second skeleton, "the Area of a Circle". Look at our second skeleton for the final term. Put all components together. Check out what you've learned and how much of it retains after that. Revisit the necessary sections if needed.
/*
A Rust program to calculate the area of a circle.
*/
use std::io;
fn main() {
println!("Type the value for the radius of the circle and hit Enter:");
let mut user_submitted_radius = String::new();
let mut radius: f32 = 0.0;
let mut squired: f32 = 0.0;
let mut area: f32 = 0.0;
io::stdin().read_line(&mut user_submitted_radius)
.ok()
.expect("Couldn't read user input!");
radius = user_submitted_radius.trim().parse().expect("Invalid user input!n");
squired = radius * radius;
area = 3.14 * squired; // The formula: area = pi * r^2
println!("Area = {}", area);
}
Let's break down the third skeleton before we move on to the actual chapters.
This time, we will start with Rust to make our job easier. Only the unexplained parts will be discussed.
A simple file input/output and user input/output program in Rust:
/*
A simple Rust program to demonstrate
file input/output & user input/output.
*/
use std::env;
use std::io;
use std::io::Read;
use std::io::Write;
fn main() {
println!("Type your name and hit Enter:");
let mut ur_name = String::new();
io::stdin()
.read_line(&mut ur_name)
.ok()
.expect("Couldn't read user input!");
let mut file1 = std::fs::File::create("textfile.txt").expect("Failed to create the file!");
file1
.write_all(ur_name.as_bytes())
.expect("Failed to write to the file!");
println!("Written data to file.");
let mut file2 = std::fs::File::open("textfile.txt").unwrap();
let mut file_contents = String::new();
file2.read_to_string(&mut file_contents).unwrap();
print!(
"Read & went through the file contents. Found:\n{}",
file_contents
);
print!("\n");
let path = env::current_dir().unwrap();
println!("$PWD: {}", path.display());
}
The C version.
// Last Change: 2022-10-21 Friday: 09:13:57 PM
/*
A simple C program to demonstrate
file input/output & user input/output.
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// https://iq.opengenus.org/detect-operating-system-in-c/
#ifdef __linux__
#include <unistd.h>
#endif
int main(int argc, char *argv[]) {
char ur_name[100];
char line[1000]; /* maximum line size */
unsigned long len_o_prev_line = 0;
unsigned long len_o_current_line = 0;
unsigned int no_o_lines = 0;
FILE *file1; /* output-file pointer */
FILE *file2; /* input-file pointer */
/* Creating the file & writing to it */
char *file1_file_name = "textfile_in_c.txt"; /* output-file name */ /* use extension within double quotes */
file1 = fopen(file1_file_name, "w"); // opened file in write mode, overwriting the old one.
if(file1 == NULL) {
fprintf(stderr, "\ncouldn't open file '%s'; %s\n", file1_file_name, strerror(errno));
exit(EXIT_FAILURE);
}
else if(file1 != NULL) {
fprintf(stderr, "\nopened file '%s'; %s\n", file1_file_name, strerror(errno));
// continue here
printf("Type your name and hit Enter:\n");
/*scanf("%s", ur_name);*/
fgets(ur_name, 64, stdin); // https://stackoverflow.com/questions/8033189/scanfs-not-allowing-space-c-programming
printf("You typed: %s\n", ur_name);
fprintf(file1, "%s", ur_name);
if(fclose(file1) == EOF) { /* close output file */
fprintf(stderr, "\ncouldn't close file '%s'; %s\n", file1_file_name, strerror(errno));
exit(EXIT_FAILURE);
}
}
/* Reading the file */
char *file2_file_name = "textfile_in_c.txt"; /* input-file name */ /* use extension within double quotes */
file2 = fopen(file2_file_name, "r");
if(file2 == NULL) {
fprintf(stderr, "\ncouldn't open file '%s'; %s\n", file2_file_name, strerror(errno));
exit(EXIT_FAILURE);
}
else if(file2 != NULL) {
fprintf(stderr, "\nopened file '%s'; %s\n", file2_file_name, strerror(errno));
// continue here
while(fgets(line, sizeof line, file2) != NULL) {
len_o_current_line = strlen(line) - 1;
if(len_o_prev_line < len_o_current_line) {
len_o_prev_line = len_o_current_line;
}
printf("Line content: %s", line);
printf("Line No. %u. Lenght of the line: %lu\n", no_o_lines + 1, len_o_current_line);
no_o_lines++;
}
if(fclose(file2) == EOF) { /* close input file */
fprintf(stderr, "\ncouldn't close file '%s'; %s\n", file2_file_name, strerror(errno));
exit(EXIT_FAILURE);
}
}
// MS Windows
#ifdef __WIN32__
printf("$PWD: ");
system("echo %cd%");
printf("\n");
#endif
// Linux
#ifdef __linux__
char *present_dir;
present_dir = (char *)malloc((size_t)(1024) * sizeof(char));
if(present_dir == NULL) {
fprintf(stderr, "\ndynamic memory allocation failed\n");
exit(EXIT_FAILURE);
}
if(getcwd(present_dir, 1024) != NULL) {
fprintf(stdout, "Present Working Directory: %s\n", present_dir);
}
free(present_dir);
present_dir = NULL;
#endif
}
Don't get puzzled. I'll explain each line in detail.
We will look at the Rust version now.
Let's start without procrastination.
use std::env;
We utilised the keyword use
to bring symbols std
---> env
into the scope of our program. What will it do for us? std:
is the Standard Library. :env
is a library contained in (grouped inside of) the Standard Library. We need it for the line let path = env::current_dir().unwrap();
which will print the current working directory to the console.
use std::io;
We need the :io
library contained in the std:
for console input/output, as we've seen before.
io::stdin()
.read_line(&mut ur_name)
.ok()
.expect("Couldn't read user input!");
Read the above section in the forward direction (recap):
-
ur_name
is a mutable/changeable variable (to be used as a string variable later). We created/declared an empty string objectur_name
aslet mut ur_name = String::new();
. -
The
use
keyword -> Bring symbols into scope. Here, the Standard Library module/component:io
(namely,std::io
) has been brought into the scope of the current program. Again, we're dragging/calling the function/methodstdin()
from the Standard Library's (std:
) component/module:io
to handle user input:read_line(&mut ur_name)
. -
read_line()
returns a valueResult
after accomplishing the designated task (apart from doing the task), either anOk
if nothing goes wrong, or anErr
. TheResult
's variants areOk
andErr
. The method/functionok()
converts the value Result into a value Option (indicating how many bytes have been read), and the functionexpect()
gives either thatOk
value or shows anErr
message when it encounters an error. The functionexpect()
lets us detect where it occurs, in case of a panic. The use of thisexpect()
function is compulsory here. If the functionexpect()
gives thatOk
value, Rust allows the program to proceed further.
See Error Handling in Rust, Module std::error
for details. Thus, the functions expect()
and unwrap()
are contained in the Standard Library std:
. To know more about the ok()
function, see Module std::result
. So, the function ok()
is found in the Standard Library std:
.
NOTE: Instead of calling the method expect()
in a chained .ok().expect()
form, you could also call the method unwrap()
to make the code slightly shorter by omitting the ok()
function, as: io::stdin().read_line(&mut ur_name).unwrap();
.
io::stdin().read_line(&mut ur_name).unwrap();
There's nothing wrong either if you use ok()
before unwrap()
as shown below.
io::stdin().read_line(&mut ur_name).ok().unwrap();
The method unwrap()
unwraps a value Result
, yielding one of the two variants of the Result
, a value Ok
(or a value Some
, in the case of a value Option
). If a panic occurs, the returned value will be an Err
(or a None
for Option
), along with a panic message according to the Err
's value. When the value returned from unwrap()
is an Ok
(T
), the function unwrap()
can extract the value T
, in a fashion such as println!("Unwrap found {}", ur_name.unwrap());
.
You can choose from ok().expect()
or ok().unwrap()
to a match statement that
tests on the Ok
(T
) or Err
(E
) value outcome of the Result
type value. You don't have to understand every little bit of the working procedure of the unwrap()
function immediately. Nevertheless, the function unwrap()
can be used for quick prototyping. Move to the next line:
let mut file1 = std::fs::File::create("textfile.txt").expect("Failed to create the file!");
Before trying to understand the line mentioned above, we will have to discuss Rust's File Struct for a brief moment.
The file input/output functionality in Rust is provided by its File Struct, which represents a file.
All functions/methods in the File Struct return a variant of the io::Result
enumeration. Enumeration won't be discussed at the moment.
The table below shows the most common functions/methods in use, at a glimpse:
Sr. No. | Module | Function/Method | Signature | Description |
---|---|---|---|---|
1 | std::fs::File |
open() |
pub fn open<P: AsRef>(path: P) -> Result |
Static Method (fn). Opens a file in read-only mode. |
2 | std::fs::File |
create() |
pub fn create<P: AsRef>(path: P) -> Result |
Static Method (fn). Opens a file in write-only mode. Overwrites the file if it already exists. Otherwise, a new file is created only for writing to it. |
3 | std::fs::remove_file |
remove_file() |
pub fn remove_file<P: AsRef>(path: P) -> Result<()> |
Deletes (removes) a file from the filesystem. However, immediate deletion is not guaranteed. |
4 | std::fs::OpenOptions |
append() |
pub fn append(&mut self, append: bool) -> &mut OpenOptions |
Sets an option to open the file in append mode. That means anything new will be added to the file after its old content. |
5 | std::io::Write |
write_all() |
fn write_all(&mut self, buf: &[u8]) -> Result<()> |
Attempts to write an entire buffer into the current write session. |
6 | std::io::Read |
read_to_string() |
fn read_to_string(&mut self, buf: &mut String) -> Result |
Reads all bytes until EOF , appending them to buf . Copies contents to the buffer (memory). |
Writing to a file:
use std::io::Write;
fn main() {
let mut filename = std::fs::File::create("data.txt")
.ok()
.expect("Failed to create the file!");
filename
.write_all("Have a great rest of your day!!".as_bytes())
.ok()
.expect("Failed to write to the file!");
filename
.write_all("\nWelcome to the world of Rust!!".as_bytes())
.ok()
.expect("Failed to write to the file!");
println!("Text data has been written to the file.");
}
Text data has been written to the file.
On Windows CMD:
type data.txt
On Linux (or MSYS2 on Windows):
cat data.txt
Have a great rest of your day!!
Welcome to the world of Rust!!
With 70% of the conceptual clearances, let us discuss the remaining parts concerned with the File Operations in the sample program described above. We brought the "Write Operation functionality" into the scope of our program by writing use std::io::Write;
. filename
is a mutable variable which is responsible for creating the file data.txt
. The name of that variable could be anything else. The create()
method/function is used to create the file. The functions ok()
and expect()
are there to handle error messages. In the last line, the write_all()
function writes bytes to the file data.txt
. As before, the ok()
and the expect()
functions/methods take the responsibility for displaying errors if something goes wrong. Open the file with any plain-text editor and see what's hiding there.
Now you should ask me: Could we not choose the route of the combination of std::fs::OpenOptions
and append()
instead of creating a file and then writing to it since the file was supposed to be new and empty? Yes. However, the file was also supposed to be not present on the system. You'll have to create (create()
) the file for the first time anyway.
// A Rust program to append data to an existing file
use std::fs::OpenOptions;
use std::io::Write;
fn main() {
let mut filename = OpenOptions::new()
.append(true)
.open("data.txt")
.expect("Unable to open file!");
filename
.write_all("\nAppending this string to the file data.txt\n".as_bytes())
.expect("Write operations failed!");
println!("Data appended successfully.");
}
If you have not deleted the file data.txt
, your program will Append the string Appending this string to the file data.txt
to the file data.txt
at this stage. What if you don't have the file data.txt
on your system? You'll see error messages if you run the program. So, create the file first, and then write nothing to the file using the same method, write_all()
. Here, write_all("")
. Otherwise, you'll see a compiler warning like value assigned to 'filename' is never read
. Although you can disable this warning of unused assignments to variables by dropping a line at the beginning of your program #![allow(unused)]
, know that it is not a good practice.
warning: value assigned to `filename` is never read
--> testrst.rs:7:13
|
7 | let mut filename = std::fs::File::create("data.txt")
| ^^^^^^^^
|
= note: `#[warn(unused_assignments)]` on by default
= help: maybe it is overwritten before being read?
warning: 1 warning emitted
// A Rust program to append data to an existing file
// #![allow(unused)]
use std::fs::OpenOptions;
use std::io::Write;
fn main() {
let mut filename = std::fs::File::create("data.txt")
.ok()
.expect("Failed to create the file!");
filename
.write_all("".as_bytes()) /* write nothing*/
.expect("Write operations failed!");
filename = OpenOptions::new()
.append(true)
.open("data.txt")
.expect("Unable to open file!");
filename
.write_all("\nAppending this string to the file data.txt\n".as_bytes())
.expect("Write operations failed!");
println!("Data appended successfully.");
}
You had to create the file and make it ready for further writing.
Reading from a file: Now, we will discuss how to read file contents.
-
Type Declaration Instruction
-
Arithmetic Instruction
-
Integer and Float Conversions
-
Type Conversion in Assignments
-
Hierarchy of Operations
-
Associativity of Operators
The tutorial 'crust' is published under The MIT-0 licence.
A copy of the MIT-0 License can be found at
https://spdx.org/licenses/MIT-0.html
Or,
https://opensource.org/licenses/MIT-0.
Or,
MIT No Attribution
Copyright <YEAR> <COPYRIGHT HOLDER>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.