A regular expression, or regex, is a sequence of characters that forms a search pattern. It’s mainly used for pattern matching within strings. In Ruby, regular expressions (regex) allow us to find specific patterns within a string. Two common uses of Ruby regex are validation (such as checking email addresses) and parsing text. Regular expressions in Ruby are enclosed between two forward slashes (/).
Syntax:
# Finding the word 'hi'
"Hi there, I am using Ruby" =~ /hi/
This will return the index of the first occurrence of ‘hi’ in the string if found, otherwise, it will return nil.
Checking if a String Contains a Pattern
You can check if a string contains a regex pattern using the match method.
Example:
Match found
Checking for Specific Characters in a String
A character class lets you define a range of characters for matching. For instance, you can use [aeiou] to search for any vowel.
Example:
# Ruby program using regular expressions
# Function to check if the string contains a vowel
def contains_vowel(str)
str =~ /[aeiou]/
end
# Driver code
puts contains_vowel("Hello") # 'Hello' has a vowel, so it returns index
puts contains_vowel("bcd") # 'bcd' has no vowels, returns nil, so nothing is printed
Output:
1
Common Regular Expressions
Here are some shorthand character classes for specifying ranges:
\w is equivalent to [0-9a-zA-Z_]
\d is the same as [0-9]
\s matches any whitespace
\W matches anything not in [0-9a-zA-Z_]
\D matches anything that’s not a number
\S matches anything that’s not a whitespace
The dot character . matches any character except a newline. If you want to search for the literal . character, you need to escape it with a backslash (\.).
Example:
# Ruby program using regular expressions
str1 = "2m3"
str2 = "2.5"
# . matches any character
if str1.match(/\d.\d/)
puts "Match found"
else
puts "Not found"
end
# Escaping the dot to match only a literal '.'
if str1.match(/\d\.\d/)
puts "Match found"
else
puts "Not found"
end
# This will match because str2 contains a dot between digits
if str2.match(/\d\.\d/)
puts "Match found"
else
puts "Not found"
end
Output:
Match found
Not found
Match found
Explanation:
1. First condition: Checks if str1 contains a digit followed by any character and then another digit (\d.\d). Since str1 contains “2m3,” it matches. 2. Second condition: Escapes the dot (\.) to match a literal .. Since str1 doesn’t contain a literal dot, it does not match. 3. Third condition: Matches because str2 (“2.5”) contains a digit, followed by a literal dot, and then another digit.
In summary, Ruby regular expressions provide a powerful way to search and match patterns within strings, allowing for complex validations and parsing tasks.
Ruby Search and Replace
sub and gsub are Ruby string methods that utilize regular expressions for searching and replacing content in a string. Their in-place variants, sub! and gsub!, modify the original string directly.
sub and sub!: Replace only the first occurrence of the pattern.
gsub and gsub!: Replace all occurrences of the pattern.
The difference between sub/gsub and their in-place variants (sub!/gsub!) is that the in-place methods (sub! and gsub!) modify the string on which they are called, while sub and gsub return a new string, leaving the original one unchanged.
Example:
# Ruby program demonstrating sub and gsub methods in a string
number = "1234-567-890 # This is a number"
# Remove the comment section using sub (replaces the first occurrence)
number = number.sub!(/#.*$/, "")
puts "Number: #{number}"
# Remove all non-digit characters using gsub (replaces all occurrences)
number = number.gsub!(/\D/, "")
puts "Number: #{number}"
In programming, predicting errors and handling them effectively is crucial. Exceptions are errors that occur during runtime and disrupt the normal flow of a program. Handling exceptions properly ensures a program continues to operate even in unexpected situations.
What is an Exception?
An exception is an unexpected event that occurs during the execution of a program, disrupting its normal flow. It provides a way to handle error scenarios without stopping the entire program.
Errors vs. Exceptions
Errors:
Unexpected issues that arise during program execution.
Cannot be handled directly.
All errors are considered exceptions.
Exceptions:
Unexpected events during runtime.
Can be managed using begin-rescue blocks.
Not all exceptions are errors.
Traditional Exception Handling Approach
In the traditional approach, errors were managed using return codes. Methods would return specific values indicating failure, which would be propagated through calling routines until a function took responsibility. This made error management complex and hard to maintain.
Ruby addresses this issue by using an exception class that packages information about errors into an object. This object is passed through the calling stack until appropriate code is found to handle the error.
Exception Class & Its Hierarchy
Ruby has a built-in hierarchy of exception classes. Most exceptions are subclasses of StandardError, which represents general errors in Ruby programs. More serious exceptions fall under other classes. Users can also create their own exceptions by subclassing StandardError or its descendants. Every exception object contains a message and a stack trace.
Example of an Exception
# Ruby program to illustrate an exception
# defining two integer values
num1 = 14
num2 = 0
# attempting to divide by zero
result = num1 / num2
puts "The result is: #{result}"
Runtime Error:
source_file.rb:6:in `/': divided by 0 (ZeroDivisionError)
from source_file.rb:6:in `<main>'
Explanation
In the example above, dividing 14 by 0 triggers a ZeroDivisionError.
Creating User-Defined Exceptions
Ruby uses the raise method to create exceptions, which become instances of the Exception class or its subclasses. The rescue clause is used to handle these exceptions.
Example of User-Defined Exception
# Ruby program to create a user-defined exception
# defining a method
def raise_exception
puts 'This is before the exception is raised!'
# using raise to create an exception
raise 'Custom Exception Raised'
puts 'This line will not be executed.'
end
# calling the method
raise_exception
Output:
This is before the exception is raised!
source_file.rb:6:in `raise_exception': Custom Exception Raised (RuntimeError)
from source_file.rb:10:in `<main>'
Handling Exceptions with rescue
You can handle exceptions in Ruby using begin-rescue blocks. This structure allows the program to continue running after handling an exception.
Example of Handling Exception
# Ruby program to create and handle a user-defined exception
# defining a method
def raise_and_rescue
begin
puts 'This is before the exception is raised!'
# using raise to create an exception
raise 'Custom Exception Raised!'
puts 'This line will not be executed.'
# using rescue to handle the exception
rescue
puts 'Exception handled!'
end
puts 'Outside of the begin block!'
end
# calling the method
raise_and_rescue
Output:
This is before the exception is raised!
Exception handled!
Outside of the begin block!
Ruby Exception Handling
Exception handling in Ruby provides a way to manage unexpected events or errors that occur during the execution of a program. These errors can disrupt the normal flow of a program and are managed using various statements like begin, rescue, raise, ensure, and more. Ruby also provides an Exception class, which has different methods to handle these errors.
Syntax of Exception Handling in Ruby
The code block where an exception might be raised is enclosed in a begin and end block. The rescue clause is used to manage the exception.
Basic Syntax:
begin
# Code where an exception might be raised
rescue
# Code to handle the exception
end
Example of Exception Handling
# Ruby program to handle an exception
# defining a method
def raise_and_rescue
begin
puts 'This is before an exception occurs!'
# raising an exception
raise 'An Exception Occurred!'
puts 'This will not be printed'
rescue
puts 'Exception handled successfully!'
end
puts 'Outside the begin block!'
end
# calling the method
raise_and_rescue
Output:
This is before an exception occurs!
Exception handled successfully!
Outside the begin block!
Explanation: In this example, an exception is raised in the begin block using raise, which interrupts the program’s flow. The rescue block then catches and handles the exception, allowing the program to continue executing.
Note
You can use multiple rescue clauses to handle different exceptions. If an exception is not handled by the first rescue, the next one is tried. If no rescue matches, or if an exception occurs outside the begin block, Ruby searches up the call stack for an exception handler.
Statements Used in Exception Handling
1. retry Statement: The retry statement re-executes the code from the beginning of the begin block after catching an exception.
Syntax:
begin
# Code where an exception might be raised
rescue
# Code to handle the exception
retry
end
Note: Be cautious while using retry as it can lead to an infinite loop if not properly managed.
2. raise Statement: The raise statement is used to raise an exception in Ruby.
Syntax:
raise: Re-raises the current exception.
raise "Error Message": Creates a RuntimeError with the given message.
raise ExceptionType, "Error Message": Creates an exception of the specified type with the given message.
Example:
# Ruby program demonstrating the use of raise statement
begin
puts 'This is before an exception occurs!'
# Raising an exception
raise 'An Exception Occurred!'
puts 'This will not be printed'
end
Output:
This is before an exception occurs!
An Exception Occurred!
3. ensure Statement: The ensure block always executes regardless of whether an exception is raised or rescued. It is placed after the rescue block.
Syntax:
begin
# Code where an exception might be raised
rescue
# Code to handle the exception
ensure
# Code that always executes
end
Output:
# Ruby program demonstrating the use of the ensure statement
begin
# Raising an exception
raise 'An Exception Occurred!'
puts 'This will not be printed'
rescue
puts 'Exception handled!'
ensure
puts 'Ensure block executed'
end
Output:
Exception handled!
Ensure block executed
4. else Statement: The else block executes only if no exception is raised in the begin block. It is placed between the rescue and ensure blocks.
Syntax:
begin
# Code where an exception might be raised
rescue
# Code to handle the exception
else
# Code that executes if no exception is raised
ensure
# Code that always executes
end
Example:
# Ruby program demonstrating the use of the else statement
begin
puts 'No exception is raised here'
rescue
puts 'Exception handled!'
else
puts 'Else block executed because no exception was raised'
ensure
puts 'Ensure block executed'
end
Output:
No exception is raised here
Else block executed because no exception was raised
Ensure block executed
5. catch and throw Statements:catch and throw provide a lightweight mechanism for managing errors and jumps in Ruby. The catch block works normally until it encounters a throw. When a throw is encountered, Ruby looks for the matching catch block with the same label.
Syntax:
catch :label_name do
# Code block
throw :label_name if condition
end
Example:
# Ruby program to illustrate Class Variables
class Library
# Class variable
@@book_count = 0
def initialize(title)
# Instance variable
@title = title
end
def display_title
puts "Book Title: #{@title}"
end
def add_book
# Increment the class variable
@@book_count += 1
puts "Total books: #@@book_count"
end
end
# Creating objects
book1 = Library.new("The Art of Ruby")
book2 = Library.new("Ruby on Rails Guide")
# Calling methods
book1.display_title
book1.add_book
book2.display_title
book2.add_book
Input:
Enter a number: 1
Output:
1
Input:
Enter a number: !
Output:
nil
Catch and Throw Exception In Ruby
In Ruby, an exception is an object that belongs to the Exception class or one of its subclasses. An exception occurs when a program reaches an undefined or unexpected state. At this point, Ruby doesn’t know how to proceed, so it raises an exception. This can happen automatically or be done manually by the programmer.
The catch and throw keywords in Ruby provide a way to handle exceptions, similar to how raise and rescue work. The throw keyword generates an exception, and when it is encountered, the program’s control flow jumps to the corresponding catch statement.
Syntax
catch :label_name do
# Code block that runs until a throw is encountered
throw :label_name if condition
# This block will not execute if throw is encountered
end
The catch block is used to jump out of nested code, and it will continue executing normally until a throw statement is encountered.
Example: Basic catch and throw
# Ruby Program using Catch and Throw for Exception Handling
result = catch(:divide) do
# Code block similar to 'begin'
number = rand(2)
throw :divide if number == 0
number # set result = number if no exception is thrown
end
puts result
Output: If the random number is 0, the output will be empty since the throw :divide statement will execute and jump to the catch block. If the random number is 1, the output will be:
1
Explanation: If the random number is 0, throw :divide is executed, and nothing is returned to the catch block, resulting in an empty output. If the number is 1, the exception is not thrown, and the result variable is set to 1.
Example:
In the previous example, when the exception was thrown, the variable result was set to nil. We can change this behavior by providing a default value to the throw keyword.
# Ruby Program using Catch and Throw with a Default Value
result = catch(:divide) do
# Code block similar to 'begin'
number = rand(2)
throw :divide, 10 if number == 0
number # set result = number if no exception is thrown
end
puts result
Output:
1
Exception Handling in Ruby
In Ruby, a literal is any constant value that can be assigned to a variable. We use literals whenever we type an object directly into the code. Ruby literals are similar to those in other programming languages, with some differences in syntax and functionality.
1. Booleans and nil: Booleans are constants that represent the truth values true and false. nil is another constant that represents an “unknown” or “empty” value, and it behaves similarly to false in conditional expressions.
Example:
raise exception_type, "exception message" if condition
Output:
true
false
false
When a raise statement is executed, it invokes the rescue block. By default, raise raises a RuntimeError.
Example 1: Using raise Statement
# Ruby program to demonstrate the use of raise statement
begin
puts 'This is Before Exception Arises!'
# Using raise to create an exception
raise 'Exception Created!'
puts 'After Exception'
end
Output:
This is Before Exception Arises!
Exception Created!
Ruby Exception Handling in Threads
Threads in Ruby can also encounter exceptions. By default, only exceptions that arise in the main thread are handled, while exceptions in other threads cause those threads to terminate. The behavior of threads when an exception arises is controlled by the abort_on_exception method, which is set to false by default.
If abort_on_exception is false, an unhandled exception will abort the current thread but allow other threads to continue running. The setting can be changed by using Thread.abort_on_exception = true or by setting $DEBUG to true. Additionally, Ruby provides the ::handle_interrupt method for handling exceptions asynchronously within threads.
Example 1: Exception in Threads
# Ruby program to demonstrate exception handling in threads
#!/usr/bin/ruby
threads = []
4.times do |value|
threads << Thread.new(value) do |i|
# Raise an error when i equals 2
raise "An error occurred!" if i == 2
print "#{i}\n"
end
end
threads.each { |t| t.join }
Output:
0
3
1
main.rb:11:in `block (2 levels) in <main>': An error occurred! (RuntimeError)
Note: The Thread.join method waits for a specific thread to finish. When a Ruby program ends, all threads are terminated, regardless of their states.
Saving Exceptions in Threads
In the following example, we handle exceptions within threads to prevent program termination and continue processing other threads.
Example 2: Handling Exceptions in Threads
# Ruby program to handle exceptions in threads
#!/usr/bin/ruby
threads = []
5.times do |value|
threads << Thread.new(value) do |i|
raise "An error occurred!" if i == 3
print "#{i}\n"
end
end
threads.each do |t|
begin
t.join
rescue RuntimeError => e
puts "Exception caught: #{e.message}"
end
end
Output:
0
1
4
2
Exception caught: An error occurred!
Using abort_on_exception
Setting abort_on_exception to true will terminate the entire program as soon as an exception occurs in any thread. Once the thread with the exception is terminated, no further output will be generated.
Example 3: Using abort_on_exception
# Ruby program to demonstrate thread termination on exception
#!/usr/bin/ruby
# Set abort_on_exception to true
Thread.abort_on_exception = true
threads = []
5.times do |value|
threads << Thread.new(value) do |i|
raise "An error occurred!" if i == 3
print "#{i}\n"
end
end
# Use Thread.join to wait for threads to finish
threads.each { |t| t.join }
Output:
0
1
2
main.rb:12:in `block (2 levels) in <main>': An error occurred! (RuntimeError)
Object-Oriented Programming (OOP) is a way of organizing code around objects, which represent real-world entities. In Ruby, everything is an object—including numbers, strings, arrays, and even classes.
In OOP, a class is a blueprint, and an object is an instance of that blueprint. For example, if Vehicle is a class, then car1 and car2 are objects created from it—each with its own attribute values.
Classes and Objects in Ruby
A class defines attributes (data) and methods (behavior). An object is created from a class using .new.
Basic Class Syntax
class ClassName
end
Creating a Class with a Constructor
Ruby uses the initialize method as a constructor. It runs automatically when you create an object using .new.
class Vehicle
def initialize(make, model)
@make = make
@model = model
end
end
In Ruby, a method returns the last evaluated expression, so return is usually optional.
class Vehicle
def initialize(make, model)
@make = make
@model = model
end
def make
@make
end
def model
@model
end
end
Modifying Attributes Safely
Instead of accessing instance variables directly, you update values through methods.
class Vehicle
def initialize(make, model)
@make = make
@model = model
end
def model
@model
end
def change_model(new_model)
@model = new_model
end
end
car = Vehicle.new("Toyota", "Camry")
car.change_model("Corolla")
puts car.model
Using attr_reader, attr_writer, and attr_accessor
Ruby provides built-in helpers to create getter and setter methods.
attr_reader → getter only
attr_writer → setter only
attr_accessor → getter + setter
class Vehicle
attr_accessor :make, :model
def initialize(make, model)
@make = make
@model = model
end
end
car = Vehicle.new("Toyota", "Camry")
car.model = "Corolla"
puts car.model
Variable Scope in Ruby
Ruby supports multiple variable types.
Instance Variables (@var)
Belong to a single object.
Class Variables (@@var)
Shared by all objects of a class.
Global Variables ($var)
Accessible everywhere (not recommended for most programs).
Class Variables and Class Methods
Class variables are shared across instances. Use class methods to access them.
class Vehicle
@@count = 0
def initialize(make, model)
@make = make
@model = model
@@count += 1
end
def self.count
@@count
end
end
Vehicle.new("Toyota", "Camry")
Vehicle.new("Honda", "Accord")
puts Vehicle.count
Inheritance in Ruby
Inheritance allows a child class to reuse and extend behavior from a parent class.
class Device
def description
puts "This is a general device."
end
end
class Laptop < Device
def description
puts "This is a laptop."
end
end
Laptop.new.description
Using super in Inheritance
Use super to call the parent method.
class Device
def description
puts "This is a general device."
end
end
class Laptop < Device
def description
puts "This is a laptop."
super
end
end
Laptop.new.description
Modules and Mixins
Ruby doesn’t support multiple inheritance directly, but you can reuse behavior using modules.
Using include (instance methods)
module Features
def feature_list
puts "Supports Wi-Fi and Bluetooth."
end
end
class Laptop
include Features
end
Laptop.new.feature_list
Using extend (class methods)
module Features
def feature_list
puts "This class supports advanced features."
end
end
class Laptop
extend Features
end
Laptop.feature_list
Access Control in Ruby
Ruby controls access at the method level, using:
public (default)
private
protected
Public and Private Example
class Device
def display_info
greet
puts "Device ready."
end
private
def greet
puts "Hello!"
end
end
Device.new.display_info
Polymorphism in Ruby
Polymorphism means the same method can behave differently depending on the object.
Polymorphism via Inheritance
class Instrument
def play
puts "Playing an instrument"
end
end
class Guitar < Instrument
def play
puts "Playing the guitar"
end
end
class Piano < Instrument
def play
puts "Playing the piano"
end
end
[Instrument.new, Guitar.new, Piano.new].each(&:play)
Polymorphism via Duck Typing
Ruby cares about what an object can do, not what it is.
class Store
def order(customer)
customer.payment_type
customer.discount
end
end
class RegularCustomer
def payment_type; puts "Paying with card"; end
def discount; puts "No discount"; end
end
class PremiumCustomer
def payment_type; puts "Paying with points"; end
def discount; puts "10% discount"; end
end
store = Store.new
store.order(RegularCustomer.new)
store.order(PremiumCustomer.new)
Ruby is a fully object-oriented language where classes, objects, inheritance, modules, and polymorphism are core tools for writing maintainable code. By combining:
classes and constructors (initialize)
accessors (attr_accessor)
inheritance (< and super)
modules (include and extend)
access control (public, private, protected)
polymorphism (inheritance + duck typing)
…you can design clean, scalable Ruby applications.
A method in Ruby is a reusable block of code that performs a specific task and optionally returns a value. Methods help reduce repetition by letting you define logic once and call it multiple times.
In Ruby, methods are defined using the def keyword and closed with end. Method names are typically written in lowercase (snake_case).
Defining and Calling a Method
A method must be defined before it can be called.
Basic Syntax
def method_name
# statements
end
Example:
def welcome
puts "Hello! Welcome to Ruby Programming."
end
welcome
Output:
Hello! Welcome to Ruby Programming.
Passing Parameters to Methods
You can pass values into a method using parameters. Ruby also supports default parameter values, which are used if the caller does not provide an argument.
When the number of arguments is unknown, Ruby lets you collect them using the splat operator *.
Example-
def show_items(*items)
puts "Number of items: #{items.length}"
items.each_with_index do |item, index|
puts "Item #{index + 1}: #{item}"
end
end
show_items "Apple", "Banana", "Cherry"
show_items "Orange"
Returning Values from Methods
Ruby automatically returns the value of the last evaluated expression. You only need return when you want to exit early or return multiple values explicitly.
Eg:
def calculate_sum
num1 = 25
num2 = 75
num1 + num2
end
puts "The sum is: #{calculate_sum}"
Method Visibility in Ruby
Method visibility determines where methods can be called from. Ruby provides three access levels:
public (default): callable from anywhere
protected: callable within the class and its subclasses, often for comparisons between instances
private: callable only inside the class, and not with an explicit receiver (even self)
Public Methods Example
class Example
def public_method1
puts "public_method1 called!"
end
public
def public_method2
puts "public_method2 called!"
end
end
obj = Example.new
obj.public_method1
obj.public_method2
Protected Methods Example
class Parent
protected
def protected_method
puts "protected_method called!"
end
end
class Child < Parent
def call_protected
protected_method
end
end
Child.new.call_protected
Private Methods Example
class Example
private
def private_method
puts "private_method called!"
end
public
def public_method
private_method
end
end
Example.new.public_method
Recursion in Ruby
Recursion happens when a method calls itself to solve a problem in smaller steps. It can make certain solutions elegant, but Ruby recursion can cause stack overflow for very large inputs.
Iterative vs Recursive Array Sum
Iterative:
def iterative_sum(numbers)
sum = 0
numbers.each { |n| sum += n }
sum
end
puts iterative_sum([1, 2, 3, 4, 5])
Recursive:
def recursive_sum(numbers)
return 0 if numbers.empty?
numbers[0] + recursive_sum(numbers[1..])
end
puts recursive_sum([1, 2, 3, 4, 5])
Recursive Factorial Example
def factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
puts factorial(5)
Recursive Fibonacci Example
def fibonacci(n)
return n if n < 2
fibonacci(n - 1) + fibonacci(n - 2)
end
puts fibonacci(5)
Tip: recursive Fibonacci is slow for large values—use iteration or memoization in real projects.
Ruby Hook Methods
Hook methods are special methods triggered by events such as including a module, inheriting from a class, or calling undefined methods.
Common hook methods include:
included
prepended
extended
inherited
method_missing
included Hook
module WelcomeMessage
def self.included(target)
puts "The #{target} has been greeted with a warm welcome!"
end
end
class User
include WelcomeMessage
end
prepended Hook
module Language
def self.prepended(target)
puts "#{self} has been prepended to #{target}"
end
def description
"The language used is Ruby."
end
end
class Programming
prepend Language
end
puts Programming.new.description
extended Hook
module Framework
def self.extended(target)
puts "#{self} was extended by #{target}"
end
def description
"This framework is based on Ruby."
end
end
class Software
extend Framework
end
puts Software.description
inherited Hook
class Vehicle
def self.inherited(subclass)
puts "#{subclass} is a subclass of Vehicle"
end
end
class Car < Vehicle
end
method_missing
class Language
def method_missing(method_name, *args)
"#{method_name} is not defined for #{self}"
end
def known_method
"This method is defined."
end
end
obj = Language.new
puts obj.known_method
puts obj.unknown_method
Ruby Decision Making (if, if-else, if-else-if, ternary)
Decision-making in programming is much like making choices in real life. In a program, certain blocks of code are executed based on whether a specific condition is true or false. Programming languages use control statements to manage the flow of execution depending on these conditions. These control structures guide the direction of execution, causing the program to branch or continue based on the current state. In Ruby, the if-else structure helps in testing these conditions.
1. if Statement: In Ruby, the if statement determines whether a block of code should be executed based on a condition. If the condition evaluates to true, the associated code is executed.
Syntax:
if (condition)
# code to be executed
end
Example:
# Ruby program to demonstrate the if statement
temperature = 30
# Check if temperature is above 25 degrees
if temperature > 25
puts "It's a warm day."
end
Output:
It's a warm day.
2. if-else Statement: The if-else statement is used when one block of code should be executed if the condition is true, and another block should run if the condition is false.
Syntax:
if (condition)
# code if the condition is true
else
# code if the condition is false
end
Example:
# Ruby program to demonstrate if-else statement
time = 16
# Check if the time is past noon
if time > 12
puts "Good afternoon!"
else
puts "Good morning!"
end
Output:
Good afternoon!
3. if-elsif-else Ladder: This structure allows multiple conditions to be evaluated one after another. Once a true condition is found, the corresponding block of code is executed, and the remaining conditions are skipped.
Syntax:
if (condition1)
# code if condition1 is true
elsif (condition2)
# code if condition2 is true
else
# code if neither condition1 nor condition2 is true
end
Example:
# Ruby program to demonstrate if-elsif-else ladder
score = 85
if score < 50
puts "You failed."
elsif score >= 50 && score < 65
puts "You passed with a C grade."
elsif score >= 65 && score < 80
puts "You passed with a B grade."
elsif score >= 80 && score < 90
puts "You passed with an A grade."
else
puts "You passed with an A+ grade."
end
Output:
You passed with an A grade.
4. Ternary Operator: The ternary operator is a shorthand for simple if-else statements. It evaluates a condition and returns one of two values based on whether the condition is true or false.
Syntax:
condition ? true_value : false_value
Example:
# Ruby program to demonstrate the ternary operator
age = 20
# Ternary operator to check if the person is an adult
status = (age >= 18) ? "Adult" : "Minor"
puts status
Output:
Adult
Ruby Loops (for, while, do..while, until)
Looping is an essential concept in programming, enabling the repeated execution of a block of code based on a specific condition. Ruby, a dynamic and flexible language, offers various looping constructs to handle iterations effectively. These loops help automate tasks that require repetitive actions within a program.
The primary types of loops in Ruby include:
1. while Loop 2. for Loop 3. do..while Loop 4. until Loop
1. while Loop: In a while loop, the condition is evaluated at the beginning of the loop, and the code block is executed as long as the condition is true. Once the condition becomes false, the loop terminates. This is known as an Entry-Controlled Loop since the condition is checked before executing the loop body. It’s commonly used when the number of iterations is unknown.
Syntax:
while condition [do]
# code to be executed
end
Example:
# Ruby program demonstrating the 'while' loop
counter = 3
# Using the while loop to print a message 3 times
while counter > 0
puts "Welcome to Ruby programming!"
counter -= 1
end
Output:
Welcome to Ruby programming!
Welcome to Ruby programming!
Welcome to Ruby programming!
2. for Loop: The for loop in Ruby functions similarly to the while loop but has a more concise syntax, especially useful when the number of iterations is known in advance. It is often used to iterate over a range, array, or collection. This loop is also an Entry-Controlled Loop since the condition is evaluated before the loop begins.
Syntax:
for variable_name[, variable...] in expression [do]
# code to be executed
end
Example:
# Ruby program demonstrating the 'for' loop using a range
for num in 1..4 do
puts "Number: #{num}"
end
Output:
Number: 1
Number: 2
Number: 3
Number: 4
Example 2: Iterating Over an Array
# Ruby program demonstrating the 'for' loop with an array
fruits = ["Apple", "Banana", "Cherry"]
for fruit in fruits do
puts fruit
end
Output:
Apple
Banana
Cherry
3. do..while Loop: The do..while loop is similar to the while loop, but with one key difference: the condition is evaluated after the code block has been executed, ensuring that the loop runs at least once. This makes it an Exit-Controlled Loop.
Syntax:
loop do
# code to be executed
break if condition
end
Example:
# Ruby program demonstrating the 'do..while' loop
counter = 0
# The loop will run at least once
loop do
puts "Iteration #{counter + 1}"
counter += 1
break if counter == 3
end
Output:
Iteration 1
Iteration 2
Iteration 3
4. until Loop: The until loop in Ruby is the opposite of the while loop. It continues executing as long as the given condition remains false, and stops once the condition becomes true. Like while, this is also an Entry-Controlled Loop.
Syntax:
until condition [do]
# code to be executed
end
Example:
# Ruby program demonstrating the 'until' loop
counter = 5
# Using the until loop to print values until counter reaches 10
until counter == 10 do
puts counter
counter += 1
end
Output:
5
6
7
8
9
5. Class Variables: Class variables start with @@ and are shared across all instances of a class. They belong to the class itself rather than any individual instance. Class variables must be initialized before they are used. An uninitialized class variable will raise an error.
Example:
# Ruby program to illustrate Class Variables
class Library
# Class variable
@@book_count = 0
def initialize(title)
# Instance variable
@title = title
end
def display_title
puts "Book Title: #{@title}"
end
def add_book
# Increment the class variable
@@book_count += 1
puts "Total books: #@@book_count"
end
end
# Creating objects
book1 = Library.new("The Art of Ruby")
book2 = Library.new("Ruby on Rails Guide")
# Calling methods
book1.display_title
book1.add_book
book2.display_title
book2.add_book
Output:
Book Title: The Art of Ruby
Total books: 1
Book Title: Ruby on Rails Guide
Total books: 2
6. Global Variables: Global variables start with a $ sign and are accessible from anywhere in the Ruby program. They are not limited to a specific class or scope. By default, an uninitialized global variable has a nil value. However, excessive use of global variables can make code difficult to debug and maintain.
Example:
# Ruby program to illustrate Global Variables
# Global variable
$global_count = 5
class FirstClass
def display_count
puts "Global count in FirstClass: #$global_count"
end
end
class SecondClass
def display_count
puts "Global count in SecondClass: #$global_count"
end
end
# Creating objects
obj1 = FirstClass.new
obj2 = SecondClass.new
# Calling methods
obj1.display_count
obj2.display_count
Output:
Global count in FirstClass: 5
Global count in SecondClass: 5
Case Statement Without a Value
In Ruby, you can use a case statement without specifying a value. Instead, you can use the when clauses to evaluate different conditions directly.
Example: Case Statement Without a Value
# Ruby program demonstrating case statement without a value
str = "HelloWorld123"
# Case statement evaluating different conditions
case
when str.match(/\d/)
puts "The string contains numbers."
when str.match(/[a-zA-Z]/)
puts "The string contains letters."
else
puts "The string contains neither letters nor numbers."
end
Example:
The string contains numbers.
Output:
Global variable in FirstClass is 20
Global variable in SecondClass is 20
Case Statement in Method Call
You can use a case statement directly in a method call, where it will return a value just like any other method.
Example: Case Statement in a Method Call
# Ruby program demonstrating case statement in a method call
str = "4567"
# Case statement inside a method call to return a value
puts case
when str.match(/\d/)
"The string contains numbers."
when str.match(/[a-zA-Z]/)
"The string contains letters."
else
"The string contains neither numbers nor letters."
end
Output:
The string contains numbers.
Control Flow Statements in Ruby
Ruby provides several control flow statements in addition to loops, conditionals, and iterators. These statements allow altering the normal execution flow in a program, controlling how the code is executed based on conditions or specific cases.
Here’s a breakdown of some important Ruby control flow statements:
1. break Statement: The break statement is used to exit a loop prematurely when a certain condition is met. Typically used in loops such as while, for, and case statements, it stops the execution of the loop as soon as the condition becomes true.
Syntax:
break
Example:
# Ruby program demonstrating the break statement
i = 1
# Using a while loop
while true
if i * 6 >= 30
break # Exit the loop if condition is met
end
puts i * 6
i += 1
end
Output:
6
12
18
24
2.next Statement: The next statement is used to skip the current iteration and move to the next one in a loop. It’s similar to the continue statement in languages like C and Java.
Syntax:
next
Example:
# Ruby program demonstrating the next statement
# Using a for loop
for t in 0...10
if t == 5
next # Skip the iteration when t is 5
end
puts t
end
Output:
0
1
2
3
4
6
7
8
9
3. redo Statement: The redo statement restarts the current iteration of the loop without testing the loop’s condition again. Unlike next, which skips to the next iteration, redo causes the loop to restart the current one.
Syntax:
redo
Example:
# Ruby program demonstrating the redo statement
val = 0
while val < 4
puts val
val += 1
redo if val == 4 # Restart the loop when val equals 4
end
Output:
0
1
2
3
4
4.retry Statement (Deprecated): The retry statement was used in earlier versions of Ruby (before 1.9) to restart a loop or an iterator from the beginning. It has been removed in later versions, as it was considered a deprecated feature.
5.return Statement: The return statement is used to exit a method and optionally pass a value back to the caller. If no value is provided, it returns nil.
Example:
# Ruby program demonstrating the return statement
def my_method
val1 = 100
val2 = 200
return val1, val2 # Returning multiple values
puts "This won't be executed"
end
result = my_method
puts result
Output:
100
200
6. throw/catch Statement: The throw and catch statements are used to create an advanced control flow structure. It’s like a multi-level break that allows you to exit out of deeply nested loops or methods. throw is used to break out, while catch defines a label where control can be transferred.
Example:
# Ruby program demonstrating throw/catch control flow
def check_number(num)
throw :error if num < 10 # Exit the block if number is less than 10
puts "Number is greater than or equal to 10!"
end
catch :error do
check_number(15)
check_number(25)
check_number(5) # This will cause the throw statement to exit the block
end
puts "Code after catch block"
Output:
Number is greater than or equal to 10!
Number is greater than or equal to 10!
Code after catch block
Break and Next Statement
Break statement:
In Ruby, the break statement is used to stop the execution of a loop prematurely. It is often applied in loops like while and for, where it helps to exit the loop when a specific condition is met. Once the condition triggers the break statement, the loop terminates, and the code continues after the loop.
Syntax:
break
Example:
# Ruby program demonstrating the break statement
i = 1
# Using a while loop
while true
puts i * 3 # Print multiples of 3
i += 1
if i * 3 >= 21
break # Exit the loop if the condition is met
end
end
Output:
3
6
9
12
15
18
Explanation: In this example, the loop prints multiples of 3. Once the value i * 3 becomes 21 or greater, the break statement stops the loop.
Example:
# Another example demonstrating break statement
x = 0
# Using a while loop
while true
puts x # Print current value of x
x += 1
break if x > 3 # Stop the loop when x becomes greater than 3
end
Output:
0
1
2
3
next Statement in Ruby
The next statement is used to skip the rest of the current loop iteration and immediately proceed to the next iteration. It’s similar to the continue statement in other programming languages like C or Python. This is useful when certain conditions inside a loop should cause the loop to skip to the next step without executing further code for that iteration.
Syntax:
next
Example:
# Ruby program demonstrating the next statement
for x in 0..6
if x + 1 < 4
next # Skip the current iteration if the condition is met
end
puts "Value of x is: #{x}" # Print the value of x for other cases
end
Output:
Value of x is: 3
Value of x is: 4
Value of x is: 5
Value of x is: 6
Working with Directories in Ruby
A directory is a location where files can be stored. In Ruby, the Dir class and the FileUtils module provide various methods for managing directories, while the File class handles file operations. Double dot (..) refers to the parent directory, and single dot (.) refers to the current directory itself.
The Dir Class
The Dir class allows access to and manipulation of directory structures in Ruby. It includes methods for listing directory contents, creating directories, changing directories, and more.
Features of the Dir Class
1. Creating Directories: The mkdir method is used to create a new directory. It returns 0 if the directory is successfully created.
Syntax:
next
Examples:
# Ruby program demonstrating the next statement
for x in 0..6
if x + 1 < 4
next # Skip the current iteration if the condition is met
end
puts "Value of x is: #{x}" # Print the value of x for other cases
end
Output:
Value of x is: 3
Value of x is: 4
Value of x is: 5
Value of x is: 6
Ruby redo and retry Statement
redo statement:
The redo statement in Ruby is used to repeat the current iteration of a loop without re-evaluating the loop condition. It is useful when you want to retry the current iteration based on a specific condition. This statement can only be used inside loops.
Syntax:
redo
Example:
# Ruby program demonstrating redo statement
restart = false
# Using a for loop
for x in 2..20
if x == 15
if restart == false
# Print message when x is 15 for the first time
puts "Re-doing when x = #{x}"
restart = true
# Using redo statement to repeat the current iteration
redo
end
end
puts x
end
The retry statement is used to restart the entire loop from the beginning. It is typically used within a block that includes begin-rescue error handling. The control jumps back to the start of the block or the loop, retrying the entire process. As of Ruby 1.9, the retry statement is considered deprecated for regular loops but is still available in begin-rescue blocks for exception handling.
Example :
# Ruby program demonstrating retry statement
# Using a do loop
10.times do |i|
begin
puts "Iteration #{i}"
raise if i > 2 # Raise an exception if i is greater than 2
rescue
# Retry the iteration if an exception is raised
retry
end
end
Keywords or reserved words are special words in a programming language that have predefined meanings and are used for certain internal processes. These words cannot be used as identifiers such as variable names, object names, or constants. Attempting to use these reserved words as identifiers will result in a compile-time error.
Example of Invalid Use of Keywords
# Ruby program to illustrate Keywords
# This is an incorrect use of the keyword 'if'
# It cannot be used as a variable name
if = 30
# Here 'if' and 'end' are keywords used incorrectly
# Using them will result in a syntax error
if if >= 18
puts "You are eligible to drive."
end
Compile-Time Error:
Error(s), warning(s):
example.rb:4: syntax error, unexpected '='
if = 30
^
example.rb:9: syntax error, unexpected '>='
if if >= 18
^
example.rb:11: syntax error, unexpected keyword_end, expecting end-of-input
Common Ruby Keywords
Ruby has a total of 41 reserved keywords. Here are some of the most common ones and their uses:
Keyword
Description
__ENCODING__
The script encoding of the current file.
__LINE__
The line number in the current file.
__FILE__
The path to the current file.
BEGIN
Runs code before any other in the current file.
END
Runs code after all other code in the current file.
alias
Creates an alias for a method.
and
Logical AND with lower precedence than &&.
class
Defines a new class.
def
Defines a method.
do
Begins a block of code.
end
Ends a syntax block such as a class, method, or loop.
if
Conditional statement.
module
Defines a module.
next
Skips to the next iteration of a loop.
nil
Represents “no value” or “undefined”.
return
Exits from a method and optionally returns a value.
self
Refers to the current object.
true
Boolean true value.
while
Creates a loop that executes while a condition is true.
Example of Using Keywords Correctly
Here’s a simple example demonstrating the correct use of some Ruby keywords:
# Ruby program to illustrate the use of Keywords
#!/usr/bin/ruby
# Defining a class named 'Person'
class Person
# Defining a method using 'def' keyword
def introduce
# Printing a statement using 'puts'
puts "Hello! I'm learning Ruby."
end
# End of the method
end
# End of the class
end
# Creating an object of the class 'Person'
student = Person.new
# Calling the method using the object
student.introduce
Output:
Hello! I'm learning Ruby.
Ruby Data Types
In Ruby, data types represent various kinds of data such as text, numbers, symbols, arrays, etc. Since Ruby is a pure Object-Oriented Language, all data types are based on classes. Here are the primary data types in Ruby:
1. Numbers: A number is generally defined as a sequence of digits, which may include a dot for decimal places. Ruby supports both integer and floating-point numbers. Depending on their size, numbers can be categorized into Fixnum and Bignum. However, in modern Ruby versions, they have been unified under the Integer class.
Example:
# Ruby program to illustrate Numbers Data Type
# Float type
distance = 0.2
# Both integer and float type
time = 15.0 / 3600
speed = distance / time
puts "The average speed of the cyclist is #{speed} km/h"
Output:
The average speed of the cyclist is 48.0 km/h
2. Boolean: The Boolean data type represents two values: true or false. This data type is used for simple true/false conditions.
Example:
# Ruby program to illustrate Boolean Data Type
if false
puts "This won't be printed!"
else
puts "This is False!"
end
if nil
puts "nil is True!"
else
puts "nil is False!"
end
if 1
puts "1 is True!"
else
puts "1 is False!"
end
Output:
This is False!
nil is False!
1 is True!
3. Strings: A string is a collection of characters enclosed within either single (') or double (") quotes. Double-quoted strings allow for string interpolation and special character sequences, while single-quoted strings only support limited escape sequences.
Example:
# Ruby program to illustrate Strings Data Type
puts "Ruby String Data Type"
puts 'Escape using "\\"'
puts 'It\'s a beautiful day!'
Output:
Ruby String Data Type
Escape using "\"
It's a beautiful day!
4. Hashes: A hash is a collection of key-value pairs, similar to a dictionary in Python or an associative array in PHP. Keys and values are separated by => and each pair is enclosed within curly braces {}.
Example:
# Ruby program to illustrate Hashes Data Type
colors = { "red" => "#FF0000", "green" => "#00FF00", "blue" => "#0000FF" }
colors.each do |color, hex|
puts "#{color} has hex value #{hex}"
end
Output:
red has hex value #FF0000
green has hex value #00FF00
blue has hex value #0000FF
5. Arrays: An array is an ordered collection of elements, which can contain data of any type. Elements are separated by commas and enclosed within square brackets []. Arrays in Ruby can hold mixed data types.
Example
# Ruby program to illustrate Arrays Data Type
data = ["Alice", 25, 5.5, "Hello, world!", "last item"]
data.each do |element|
puts element
end
Output:
Alice
25
5.5
Hello, world!
last item
6. Symbols: Symbols are lightweight, immutable strings. They are used instead of strings in situations where memory optimization is crucial. A symbol is prefixed with a colon (:).
Example:
# Ruby program to illustrate Symbols Data Type
countries = { :us => "United States", :ca => "Canada", :fr => "France" }
puts countries[:us]
puts countries[:ca]
puts countries[:fr]
Output:
United States
Canada
France
Types of Variables in Ruby
In Ruby, there are four main types of variables, each serving different purposes and having different scopes. Each type is distinguished by a special character at the beginning of the variable name.
Symbol
Type of Variable
[a-z] or _
Local Variable
@
Instance Variable
@@
Class Variable
$
Global Variable
1. Local Variables: Local variables in Ruby start with a lowercase letter (a-z) or an underscore (_). They are restricted to the scope in which they are defined (e.g., inside a method or a block). Local variables do not need to be initialized before use.
Example:
# Ruby program to illustrate Local Variables
# Local variables
name = "Alice"
_age = 30
puts "Name: #{name}"
puts "Age: #{_age}"
Output:
Name: Alice
Age: 30
2. Instance Variables: Instance variables start with an @ sign. They belong to a specific instance of a class and are accessible across different methods within that instance. Each instance of the class can have different values for its instance variables. Uninitialized instance variables have a nil value by default.
Example:
# Ruby program to illustrate Instance Variables
class Book
def initialize(title, author)
# Instance variables
@title = title
@auth_name = author
end
def display_info
puts "Title: #{@title}"
puts "Author: #{@auth_name}"
end
end
# Creating objects
book1 = Book.new("Ruby Programming", "John Doe")
book2 = Book.new("Learning Ruby", "Jane Doe")
# Calling methods
book1.display_info
book2.display_info
Output:
Title: Ruby Programming
Author: John Doe
Title: Learning Ruby
Author: Jane Doe
3. Class Varia: Class variables start with @@ and are shared across all instances of a class. They belong to the class itself rather than any individual instance. Class variables must be initialized before they are used. An uninitialized class variable will raise an error.
Example:
# Ruby program to illustrate Class Variables
class Library
# Class variable
@@book_count = 0
def initialize(title)
# Instance variable
@title = title
end
def display_title
puts "Book Title: #{@title}"
end
def add_book
# Increment the class variable
@@book_count += 1
puts "Total books: #@@book_count"
end
end
# Creating objects
book1 = Library.new("The Art of Ruby")
book2 = Library.new("Ruby on Rails Guide")
# Calling methods
book1.display_title
book1.add_book
book2.display_title
book2.add_book
Output:
Book Title: The Art of Ruby
Total books: 1
Book Title: Ruby on Rails Guide
Total books: 2
4. Global Variables: Global variables start with a $ sign and are accessible from anywhere in the Ruby program. They are not limited to a specific class or scope. By default, an uninitialized global variable has a nil value. However, excessive use of global variables can make code difficult to debug and maintain.
Example:
# Ruby program to illustrate Global Variables
# Global variable
$global_count = 5
class FirstClass
def display_count
puts "Global count in FirstClass: #$global_count"
end
end
class SecondClass
def display_count
puts "Global count in SecondClass: #$global_count"
end
end
# Creating objects
obj1 = FirstClass.new
obj2 = SecondClass.new
# Calling methods
obj1.display_count
obj2.display_count
Output:
Global count in FirstClass: 5
Global count in SecondClass: 5
Global Variable in Ruby
Global variables have a global scope, meaning they are accessible from anywhere in a program. Modifying global variables from any point in the code has implications across the entire program. Global variables are always denoted with a dollar sign ($). If a variable needs to be shared across multiple classes, defining it as a global variable ensures it can be accessed from all classes. By default, an uninitialized global variable has a nil value, which can lead to confusing and complex code. Global variables can be modified from any part of the program.
Syntax:
$global_variable = 5
Example:
# Ruby program demonstrating global variables
# Defining a global variable
$global_variable = 20
# Defining first class
class FirstClass
def display_global
puts "Global variable in FirstClass is #$global_variable"
end
end
# Defining second class
class SecondClass
def display_global
puts "Global variable in SecondClass is #$global_variable"
end
end
# Creating instances of both classes
first_instance = FirstClass.new
first_instance.display_global
second_instance = SecondClass.new
second_instance.display_global
Output:
Global variable in FirstClass is 20
Global variable in SecondClass is 20
In the above example, a global variable is defined with the value 20, and it can be accessed in both classes.
Another Example:
# Ruby program to demonstrate global variables across methods and classes
$global_variable1 = "Hello"
class SampleClass
def show_global_in_instance_method
puts "Global variables are accessible here: #{$global_variable1}, #{$another_global_var}"
end
def self.set_global_in_class_method
$another_global_var = "World"
puts "Global variables are accessible here: #{$global_variable1}"
end
end
# Using the class method
SampleClass.set_global_in_class_method
# Creating an object and using the instance method
sample_object = SampleClass.new
sample_object.show_global_in_instance_method
Output:
Global variables are accessible here: Hello
Global variables are accessible here: Hello, World
Literal
In Ruby, a literal is any constant value that can be assigned to a variable. We use literals whenever we type an object directly into the code. Ruby literals are similar to those in other programming languages, with some differences in syntax and functionality.
1. Booleans and nil: Booleans are constants that represent the truth values true and false. nil is another constant that represents an “unknown” or “empty” value, and it behaves similarly to false in conditional expressions.
2. Numbers: Ruby supports different types of numbers, including integers and floating-point numbers. You can write numbers of any size, using underscores (_) for readability. Ruby allows various numerical formats, including decimal, hexadecimal, octal, and binary.
3. Strings: Strings in Ruby can be enclosed in either double quotes (" ") or single quotes (' '). Double-quoted strings support interpolation and special characters, while single-quoted strings only support limited escape sequences.
Example:
# String literals demonstration
puts("The result of 3 + 4 is: #{3 + 4}") # string interpolation
puts("Hello\nWorld") # new line in double quotes
puts('Hello\nWorld') # no new line in single quotes
Output:
The result of 3 + 4 is: 7
Hello
World
Hello\nWorld
4. Symbols: A symbol in Ruby is a lightweight, immutable string used as an identifier. Symbols are created using a colon (:) and are never garbage-collected, making them memory-efficient for repeated use.
Example:
# Symbol demonstration
puts(:user_id) # simple symbol
puts(:"user#{42}") # symbol with interpolation
Output:
user_id
user42
5. Ranges: Ranges represent a set of values between a starting and an ending point. They can include numbers, characters, or other objects. Ranges can be constructed using the .. (inclusive) or ... (exclusive) operators.
Example:
# Range literals demonstration
for i in 1..4 do
puts(i)
end
puts("Exclusive range:", (1...4).to_a)
Output:
1
2
3
4
Exclusive range: [1, 2, 3]
6. Arrays: An array is a collection of objects in Ruby, enclosed within square brackets ([ ]). Arrays can contain objects of different data types, and elements are separated by commas.
Example:
# Array demonstration
fruits = ['apple', 'banana', 'cherry']
puts(fruits[0]) # accessing the first element
puts(fruits[1..2]) # accessing a range of elements
puts("Negative index:", fruits[-1]) # accessing the last element
Output:
apple
banana
cherry
Negative index: cherry
7. Hashes: A hash is a collection of key-value pairs, similar to dictionaries in other languages. Hashes are defined using curly braces ({ }), and symbols can be used as keys for efficiency.
Example:
# Hash demonstration
person = { name: "Alice", age: 30, city: "Wonderland" }
# Accessing hash elements
puts(person[:name])
puts(person[:age])
# Iterating over the hash
person.each do |key, value|
puts("#{key} => #{value}")
end
Output:
Alice
30
name => Alice
age => 30
city => Wonderland
8. Regular Expressions: Ruby supports regular expressions (regex) for pattern matching. Regular expressions are defined using forward slashes (/pattern/) or %r{pattern}.
Example:
# Regular expression demonstration
line1 = "The quick brown fox"
line2 = "Jumps over the lazy dog"
# Checks if 'quick' is in line1
if (line1 =~ /quick/)
puts line1
end
# Checks if 'lazy' is in line2 using %r{} format
if (line2 =~ %r{lazy})
puts line2
end
# Checks if 'rabbit' is in line2
if (line2 =~ /rabbit/)
puts line2
else
puts "No match found"
end
Output:
The quick brown fox
Jumps over the lazy dog
No match found
Ruby Directories
Working with Directories in Ruby
A directory is a location where files can be stored. In Ruby, the Dir class and the FileUtils module provide various methods for managing directories, while the File class handles file operations. Double dot (..) refers to the parent directory, and single dot (.) refers to the current directory itself.
The Dir Class
The Dir class allows access to and manipulation of directory structures in Ruby. It includes methods for listing directory contents, creating directories, changing directories, and more.
Features of the Dir Class
1. Creating Directories: The mkdir method is used to create a new directory. It returns 0 if the directory is successfully created.
Syntax:
Dir.mkdir "directory_name"
Examples:
# Creating a directory named "my_folder"
result = Dir.mkdir "my_folder"
# Output the result
puts result
Output:
2. Checking Directories: The exist? method checks if a directory exists.
Syntax:
Dir.exist? "directory_name"
Example:
# Create a directory named "my_folder"
Dir.mkdir("my_folder")
# Check if the directory exists
puts Dir.exist?("my_folder")
Output:
true
The empty? method checks if a directory is empty.
Syntax:
Dir.empty? "directory_name"
3. Working with Directories
The new method creates a new directory object (the directory should already exist).
Syntax:
obj = Dir.new("directory_name")
The pwd method returns the current working directory.
Syntax:
Dir.pwd
Example:
# Create a directory named "example_folder"
Dir.mkdir("example_folder")
# Print the current working directory
puts Dir.pwd
Output:
/path/to/current/directory
The home method returns the home directory of the current user.
Syntax:
Dir.home
Example:
# Print the home directory
puts Dir.home
Output:
/Users/username
The path method returns the path of a directory object.
Syntax:
d = Dir.new("directory_name")
d.path
Output:
Odd
Example:
# Create a directory and get its path
Dir.mkdir("example_folder")
obj = Dir.new("example_folder")
puts obj.path
Output:
example_folder
The getwd method returns the path of the current directory.
Syntax:
Dir.getwd
Example:
/path/to/current/directory
The chdir method changes the current working directory.
The entries method lists all files and folders in a directory.
Syntax:
Dir.entries("directory")
Example:
# Create a directory and subdirectories
Dir.mkdir("main_folder")
Dir.chdir("main_folder")
Dir.mkdir("subfolder1")
Dir.mkdir("subfolder2")
# List all files and folders
puts Dir.entries(".")
Output:
.
..
subfolder1
subfolder2
The glob method lists files matching a certain pattern.
Syntax:
Dir.glob("pattern")
Example:
# Create files and folders
Dir.mkdir("sample_folder")
Dir.chdir("sample_folder")
Dir.mkdir("test")
Dir.mkdir("demo.rb")
Dir.mkdir("script.rb")
# List files matching patterns
puts Dir.glob("*.rb")
Output:
demo.rb
script.rb
4. Removing Directories: Methods like rmdir, delete, and unlink are used to remove directories.
6. Moving Files and Folders: The mv method from the FileUtils module is used to move files or directories.
Syntax:
FileUtils.mv("source", "destination")
Example:
require "fileutils"
# Create directories
Dir.mkdir "source_folder"
Dir.mkdir "destination_folder"
# Move source_folder into destination_folder
FileUtils.mv("source_folder", "destination_folder")
# Check if the move was successful
Dir.chdir("destination_folder")
puts Dir.exist?("source_folder")
Output:
true
7. Copying Files: The cp method from the FileUtils module copies files from one location to another.
Syntax:
FileUtils.cp("source", "destination")
Example:
require "fileutils"
# Copy file.txt from source_folder to destination_folder
FileUtils.cp("source_folder/file.txt", "destination_folder")
# Check if the file exists in the destination
Dir.chdir("destination_folder")
puts File.exist?("file.txt")
Ruby is a pure object-oriented programming language created by Yukihiro Matsumoto, commonly known as Matz within the Ruby community, in the mid-1990s in Japan. In Ruby, almost everything is treated as an object, with the exception of blocks, which can be replaced by constructs like procs and lambdas. The main goal behind Ruby’s development was to create a language that acts as an intuitive bridge between human programmers and the underlying computing systems. Ruby’s syntax bears resemblance to languages such as C and Java, making it easy for developers from those backgrounds to pick up. It runs across various platforms, including Windows, Mac, and Linux.
Ruby draws inspiration from multiple programming languages, including Perl, Lisp, Smalltalk, Eiffel, and Ada. It is an interpreted scripting language, meaning its instructions are executed directly, without the need for prior compilation into machine code. Ruby programmers also benefit from access to RubyGems, a package manager providing a standard format for distributing Ruby libraries and applications.
Getting Started with Ruby Programming
1. Finding a Compiler: Before you can start coding in Ruby, you need a compiler to run your programs. There are many online Ruby compilers that allow you to begin programming without installation:
JDoodle
Repl.it
2. Writing a Ruby Program: Ruby is easy to learn, particularly for those familiar with other common programming languages, due to its similar syntax.
Writing Ruby Programs: You can write Ruby code in any text editor, such as Notepad++ or gedit. Once the code is written, save the file with the .rb extension.
Key Concepts:
Comments: Single-line comments in Ruby are created using the # symbol.Example:
fun main() {
println("Hello World")
}
Key Features of Kotlin:
1. Statically Typed: In Kotlin, the type of every variable and expression is determined at compile time. Although it is a statically typed language, it doesn’t require you to explicitly define the type for every variable.
2. Data Classes: Kotlin includes Data Classes that automatically generate boilerplate code such as equals, hashCode, toString, and getters/setters. For example:
Java Code:
class Book {
private String title;
private Author author;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Author getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
}
Kotlin Code:
data class Book(var title: String, var author: Author)
Conciseness: Kotlin significantly reduces the amount of code required compared to other object-oriented programming languages.
Safety: Kotlin helps prevent the notorious NullPointerExceptions by incorporating nullability into its type system. By default, every variable in Kotlin is non-null.
val s: String = "Hello World" // Non-null
// The following line will produce a compile-time error:
// s = null
Output:
Hello, World! Welcome to Ruby Programming.
Advantages of Ruby
Concise and Elegant Code: Ruby allows you to write clean, efficient, and powerful code in fewer lines compared to many other programming languages.
Rapid Web Development: Ruby excels at web development, reducing the effort required to build applications quickly.
Open Source: Ruby is free to use, modify, and distribute, providing flexibility to developers to make necessary changes.
Dynamic Language: Ruby’s dynamic nature allows developers to implement features without rigid constraints, and its syntax closely resembles natural language.
Disadvantages of Ruby
Learning Curve: Ruby has a unique syntax that may take time for programmers to get accustomed to, especially those who are used to other languages.
Debugging Challenges: Since Ruby is dynamically typed and many errors occur at runtime, debugging can be more challenging compared to statically typed languages.
Limited Resources: Ruby doesn’t have as many learning materials and community resources compared to more established languages like Python or Java.
Performance: As Ruby is an interpreted language, it tends to be slower than compiled languages like C++ or Java.
Comparison of Java with other programming languages
Python vs. Java
High-Level Language: Python is a high-level programming language that fully supports object-oriented programming, though it is not a pure object-oriented language.
Interpretation vs. Compilation: Python is an interpreted language, whereas Java is compiled, giving Java an edge in performance.
Scripting Language: Python is classified as a scripting language, while Java is considered a low-level implementation language.
Ease of Use: Python’s syntax is simple and concise, which makes it easier to learn and use compared to Java. Python programs typically require fewer lines of code, while Java can be more verbose.
Popularity: Python is widely used in industries for various projects because of its simplicity, while Java’s complexity can make it more challenging, though it remains popular for large-scale applications.
Dynamic Typing: Python supports dynamic typing, allowing developers to save time by writing less code. In contrast, Java requires static typing, where the type of each variable must be defined before use.
Performance: Python programs generally run slower than Java programs, but Python is often chosen for rapid prototyping and projects with faster development timelines.
Library Support: Java provides extensive libraries that are better suited for certain use cases, especially when performance and security are priorities.
C++ vs. Java
Language Paradigm: C++ is both a procedural and object-oriented language, while Java is strictly object-oriented.
Different Objectives: C++ is designed primarily for system-level programming, whereas Java is designed with cross-platform network computing in mind.
Operator Overloading: Java does not support operator overloading, while C++ does.
Memory Management: Java features automatic garbage collection, whereas in C++, memory management is manual, requiring developers to destroy objects explicitly.
Execution Speed: C++ generally runs faster than Java due to its closer proximity to machine code.
Libraries: C++ libraries are more focused on system-level tasks, while Java’s cross-platform libraries provide robust support for a wide variety of applications.
Pointers: C++ allows for the use of pointers (variables that store memory addresses), which Java intentionally omits to reduce complexity and improve security.
Ruby vs. Java
Typing: Java is statically typed, requiring explicit declaration of variable types, while Ruby is dynamically typed, allowing more flexibility but potentially leading to more runtime errors.
Code Execution: Java code is compiled into bytecode and run on the Java Virtual Machine (JVM), making it faster in execution compared to Ruby’s interpreted nature.
Code Efficiency: Ruby allows for more concise code, requiring fewer lines than Java for similar functionality, which is why Ruby is often favored for rapid development.
Inheritance and Access Modifiers: Both Java and Ruby support inheritance and include public, private, and protected methods, but Ruby’s dynamic typing often results in simpler, more flexible code.
C vs. Java
Procedural vs. Object-Oriented: C is a procedural language that focuses on structured programming, while Java follows the object-oriented paradigm.
Execution Speed: Programs written in C generally execute faster than those in Java, as C compiles directly to machine code.
Pointer Support: C supports pointers, a feature that Java omits to improve safety and simplify memory management.
Exception Handling: Java has robust exception handling mechanisms, which C lacks, making error management in Java programs more efficient and user-friendly.
Similarities Between Ruby and C
Despite their differences, Ruby and C share several key similarities, including:
1. Procedural Programming: Both languages allow programmers to write code procedurally if desired, even though Ruby operates in an object-oriented manner behind the scenes. 2. Common Operators: Both Ruby and C use similar operators such as compound assignment and bitwise operators. However, Ruby lacks the ++ and — operators that are present in C. 3. Special Variables: Both languages provide special variables like __FILE__ and __LINE__. 4. Constants: While neither language uses a specific const keyword, both support the use of constants. 5. String Notation: In both languages, strings are written within double quotes, e.g., “Hello”. 6. Mutable Strings: Both Ruby and C feature mutable strings, allowing modification after creation. Documentation: Ruby’s ri command functions similarly to the man pages in C, offering a way to view documentation directly in the terminal. Debuggers: Both languages feature similar command-line debuggers.
Differences Between Ruby and C
Though they share similarities, Ruby and C differ significantly in various aspects:
Ruby
C
Code does not need to be compiled; it can be executed directly.
Code must be compiled before it can be executed.
Ruby uses require 'foo' instead of #include or #include "foo".
C uses #include to include files or libraries.
Variables do not need to be declared before use.
Variables must be declared before use.
Ruby lacks macros, a pre-processor, casts, pointers, typedefs, sizeof, and enums.
C includes all these features.
Arguments to methods (functions) are passed by value, with values always being object references.
C allows passing function arguments by value and by reference.
Parentheses around method calls are often optional.
Parentheses are required for function calls in C.
Ruby does not have a char type—single characters are treated as 1-letter strings.
C uses the char data type to represent single characters.
Array literals are enclosed in square brackets.
Array literals are enclosed in curly braces.
Ruby does not allow dropping down to assembly.
C allows dropping down to assembly for low-level operations.
Objects in Ruby are strongly typed.
Objects in C are not strongly typed.
Parentheses are optional in if and while conditionals.
Parentheses are required for conditionals in C.
Strings do not end with a null byte.
Strings in C are null-terminated (end with a null byte).
Adding two arrays creates a new array, instead of using pointer arithmetic.
Pointer arithmetic is required when working with arrays.
Ruby arrays automatically expand as elements are added.
C arrays have fixed sizes and do not grow automatically.
Variables live on the heap, and the garbage collector handles memory management.
Memory must be manually allocated and deallocated in C, as there is no garbage collector.
Multi-line constructs like loops are closed using the end keyword, with no braces.
Braces {} are required to enclose multi-line blocks in C.
Ruby lacks header files; all functions and classes are defined directly in the source code.
C uses header files to declare functions and types.
No semicolons are required to end lines.
Lines must end with a semicolon in C.
Ruby has no #define directive; constants are used instead.
C uses #define for macros and constants.
The do keyword is used for Ruby “blocks”, but there is no do statement as in C.
C uses the do keyword with while to form do-while loops.
Similarities and Differences between Ruby and C++ language
C++ and Ruby share several similarities, including the following:
Differences Between Ruby and C++
Ruby
C++
In Ruby, variables are automatically dereferenced, meaning they point directly to objects without needing explicit references or pointers.
In C++, references and pointers are explicitly used to handle objects and their addresses.
Ruby was created by Yukihiro Matsumoto (Matz) and first released in 1996.
C++ was introduced by Bjarne Stroustrup in 1979 as an extension of the C language, with the first official release in 1985.
Ruby uses dynamic typing, where variables can hold objects of any type, and their types are checked at runtime.
C++ employs static typing, meaning variable types must be declared explicitly, and types are checked at compile time.
In Ruby, constructors are always named initialize, regardless of the class name.
C++ constructors are named after the class itself, with no fixed keyword for them.
Ruby primarily relies on two container types, Arrays and Hashes.
C++ offers a wide range of container types, including vectors, lists, maps, and more through the Standard Template Library (STL).
Ruby doesn’t require templates or casting.
C++ uses templates for generic programming and often requires explicit casting between types.
Ruby uses self to refer to the current instance of an object.
In C++, this is used to refer to the current object.
Iteration in Ruby is done using built-in iterator methods, often combined with code blocks.
In C++, iteration is typically done using iterators or looping structures like for or while loops.
Ruby includes a standard unit testing framework (lib) as part of the language.
C++ does not come with a built-in unit testing framework, though many external libraries are available.
Ruby doesn’t perform type conversion, relying on dynamic typing and flexibility with data types.
C++ often requires explicit type conversion to ensure proper handling of variable types.
Ruby enforces some naming conventions for methods and variables, like using snake_case.
C++ does not enforce any specific naming conventions, allowing developers to choose their style.
In Ruby, classes can be reopened at any time to add or modify methods dynamically.
C++ classes cannot be reopened once defined; all methods and attributes must be declared upfront.
Ruby methods can end with special characters like ? (for queries) or ! (for methods that modify the object).
C++ methods do not use special symbols like ? or ! in their names.
All methods in Ruby are virtual, allowing for polymorphism without additional syntax.
In C++, methods must be explicitly declared as virtual to support polymorphism.
Ruby has built-in support for multithreading, although early versions (e.g., 1.8) used green threads.
C++ does not include multithreading as part of the core language, though it can be implemented using libraries.
In Ruby, parentheses for method calls are optional, making the syntax more flexible.
C++ requires parentheses for all function and method calls, regardless of context.
Ruby prohibits direct access to member variables; all access must go through getter and setter methods.
In C++, member variables can be accessed directly unless they are explicitly made private or protected.
Ruby is predominantly used for web development, automation, and scripting.
C++ is widely used for systems programming, game development, embedded systems, and large-scale applications.
Ruby runs on platforms like Linux, macOS, and Solaris.
C++ supports a wide range of operating systems, including Windows, macOS, Linux, and many others.
Basic Ruby Example
fun main() {
println("Hello, Kotlin!")
}
Hello World Program:
# Define a class
class Person
# Constructor method
def initialize(name, age)
@name = name
@age = age
end
# Method to display person details
def display_details
puts "Name: #{@name}, Age: #{@age}"
end
end
# Create an object of the Person class
person = Person.new("Alice", 30)
person.display_details # Output: Name: Alice, Age: 30
Basic Control Structures:
# If-else statement
number = 10
if number > 5
puts "Number is greater than 5"
else
puts "Number is less than or equal to 5"
end
# Looping with 'each'
[1, 2, 3, 4, 5].each do |num|
puts num
end
Functions:
# Define a function
def greet(name)
return "Hello, #{name}!"
end
# Call the function
puts greet("Alice") # Output: Hello, Alice!
Ruby is a dynamic, object-oriented, and open-source programming language known for its simplicity, readability, and developer productivity. It is widely used for web development, scripting, automation, and backend development (notably with Ruby on Rails).
Multithreading is a feature in C++ that allows multiple threads to run concurrently, making better use of the CPU. Each thread is a separate flow of execution within a process, which allows multiple parts of a program to run in parallel.
Multithreading support was added in C++11, and before that, programmers had to use the POSIX threads (pthreads) library. C++11 introduced std::thread, which made multithreading much easier and portable across different platforms. The std::thread class and related utilities are provided in the <thread> header.
Syntax for Creating a Thread:
std::thread thread_object(callable);
Here, std::thread represents a single thread in C++. To start a new thread, we create a std::thread object and pass a callable to its constructor. A callable can be:
1. A Function Pointer 2. A Lambda Expression 3. A Function Object (Functor) 4. A Non-Static Member Function 5. A Static Member Function
Once the callable is passed, the thread will execute the corresponding code.
Launching a Thread Using a Function Pointer
A function pointer can be passed to a std::thread constructor to launch a thread:
void my_function(int value)
{
for (int i = 0; i < value; i++) {
std::cout << "Thread using function pointer\n";
}
}
// Creating and launching the thread
std::thread my_thread(my_function, 5);
Launching a Thread Using a Lambda Expression
A lambda expression is a convenient way to define a callable on the fly. Here’s how to use it to launch a thread:
auto my_lambda = [](int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using lambda expression\n";
}
};
// Launching a thread using the lambda expression
std::thread my_thread(my_lambda, 5);
Launching a Thread Using a Function Object (Functor)
A function object (functor) is a class with an overloaded () operator. Here’s an example:
class Functor {
public:
void operator()(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using functor\n";
}
}
};
// Launching a thread using a function object
std::thread my_thread(Functor(), 5);
Output:
a < b : 0
a > b : 1
a <= b: 0
a >= b: 1
a == b: 0
a != b : 1
Launching a Thread Using a Non-Static Member Function
Non-static member functions require an instance of the class to be called. Here’s how to use a non-static member function in a thread:
class MyClass {
public:
void my_method(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using non-static member function\n";
}
}
};
// Creating an instance of the class
MyClass my_obj;
// Launching the thread
std::thread my_thread(&MyClass::my_method, &my_obj, 5);
Launching a Thread Using a Static Member Function
Static member functions do not require an instance of the class and can be directly passed to a thread:
class MyClass {
public:
static void my_static_method(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using static member function\n";
}
}
};
// Launching the thread using the static member function
std::thread my_thread(&MyClass::my_static_method, 5);
Waiting for Threads to Finish
Once a thread is launched, we may need to wait for it to finish before proceeding. The join() function blocks the calling thread until the specified thread completes execution.
int main() {
std::thread t1(my_function, 5);
t1.join(); // Wait for t1 to finish
// Proceed with other tasks after t1 finishes
}
Complete C++ Program for Multithreading
Below is a complete C++ program that demonstrates launching threads using different callables, including a function pointer, lambda expression, functor, and member functions:
#include <iostream>
#include <thread>
// Function to be used as a function pointer
void function_pointer(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using function pointer\n";
}
}
// Functor (Function Object)
class Functor {
public:
void operator()(int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using functor\n";
}
}
};
// Class with member functions
class MyClass {
public:
void non_static_function() {
std::cout << "Thread using non-static member function\n";
}
static void static_function() {
std::cout << "Thread using static member function\n";
}
};
int main() {
std::cout << "Launching threads...\n";
// Launch thread using function pointer
std::thread t1(function_pointer, 3);
// Launch thread using functor
Functor functor;
std::thread t2(functor, 3);
// Launch thread using lambda expression
auto lambda = [](int value) {
for (int i = 0; i < value; i++) {
std::cout << "Thread using lambda expression\n";
}
};
std::thread t3(lambda, 3);
// Launch thread using non-static member function
MyClass obj;
std::thread t4(&MyClass::non_static_function, &obj);
// Launch thread using static member function
std::thread t5(&MyClass::static_function);
// Wait for all threads to finish
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
std::cout << "All threads finished.\n";
return 0;
}
Output:
Launching threads...
Thread using function pointer
Thread using function pointer
Thread using function pointer
Thread using lambda expression
Thread using lambda expression
Thread using lambda expression
Thread using functor
Thread using functor
Thread using functor
Thread using non-static member function
Thread using static member function
All threads finished.
Pointers in C++
Pointers in C++ are used to access external resources, such as heap memory. When you access an external resource without a pointer, you only interact with a copy, meaning changes to the copy won’t affect the original resource. However, when you use a pointer, you can modify the original resource directly.
Common Issues with Normal Pointers
1. Memory Leaks: Occur when memory is allocated but not freed, leading to wasted memory and potential program crashes. 2. Dangling Pointers: Happen when a pointer refers to memory that has already been deallocated. 3. Wild Pointers: Pointers that are declared but not initialized to point to a valid address. 4. Data Inconsistency: Happens when memory data is not updated uniformly across the program. 5. Buffer Overflow: Occurs when writing outside of allocated memory, which can corrupt data or cause security issues.
Example of Memory Leak:
#include <iostream>
using namespace std;
class Demo {
private:
int data;
};
void memoryLeak() {
Demo* p = new Demo();
}
int main() {
while (true) {
memoryLeak();
}
return 0;
}
Explanation: In the memoryLeak function, a pointer to a dynamically created Demo object is created, but the memory allocated by new is never deallocated using delete, leading to a memory leak as the program keeps allocating memory without freeing it.
Smart Pointers
Smart pointers in C++ automatically manage memory allocation and deallocation, avoiding manual delete calls and preventing memory leaks. Unlike normal pointers, smart pointers automatically free the memory when they go out of scope. Smart pointers overload operators like * and -> to behave similarly to normal pointers but with additional memory management features.
C++ provides several types of smart pointers that are available in the standard library:
1. unique_ptr: Manages a single object and ensures that only one unique_ptr instance can point to a particular object. 2. shared_ptr: Allows multiple shared_ptr objects to share ownership of the same object, managing reference counting. 3. weak_ptr: A non-owning smart pointer that is used in conjunction with shared_ptr to avoid circular references.
Example: Using unique_ptr
Area: 48
Area (after transfer): 48
Explanation: The ownership of the object is transferred from up1 to up2 using std::move. After the transfer, up1 becomes null, and up2 owns the object.
Example: Using shared_ptr
#include <iostream>
#include <memory>
using namespace std;
class Circle {
int radius;
public:
Circle(int r) : radius(r) {}
int circumference() { return 2 * 3.14 * radius; }
};
int main() {
shared_ptr<Circle> sp1(new Circle(7));
cout << "Circumference: " << sp1->circumference() << endl;
shared_ptr<Circle> sp2 = sp1;
cout << "Reference count: " << sp1.use_count() << endl;
return 0;
}
Output:
Circumference: 43.96
Reference count: 2
Explanation: Both sp1 and sp2 share ownership of the Circle object, and the reference count is managed automatically.
Example: Using weak_ptr
#include <iostream>
#include <memory>
using namespace std;
class Box {
int size;
public:
Box(int s) : size(s) {}
int getSize() { return size; }
};
int main() {
shared_ptr<Box> sp1(new Box(15));
weak_ptr<Box> wp1(sp1); // weak_ptr does not increase reference count
cout << "Box size: " << sp1->getSize() << endl;
cout << "Reference count: " << sp1.use_count() << endl;
return 0;
}
Output:
Box size: 15
Reference count: 1
auto_ptr vs unique_ptr vs shared_ptr vs weak_ptr in C++
Smart pointers in C++ are special objects that manage memory and resources automatically. They are part of the C++ Standard Library and are defined in the <memory> header. The key types of smart pointers are:
These smart pointers are used to manage dynamic memory and other resources, ensuring proper cleanup and preventing issues such as memory leaks and dangling pointers.
1. auto_ptr :auto_ptr was a smart pointer in C++ before C++11 but was deprecated because of its limitations. It manages the memory of dynamically allocated objects and automatically deletes the object when the auto_ptr goes out of scope. However, auto_ptr follows a transfer-of-ownership model, meaning only one pointer can own an object at a time. Copying or assigning an auto_ptr transfers ownership, making the original pointer empty.
Example:
(10 * 5) + (8 / 2) = 50 + 4 = 54
Example 1: C Program to Calculate the Area and Perimeter of a Rectangle
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
public:
void display() { cout << "MyClass::display()" << endl; }
};
int main() {
auto_ptr<MyClass> ptr1(new MyClass);
ptr1->display();
// Transfer ownership to ptr2
auto_ptr<MyClass> ptr2(ptr1);
ptr2->display();
// ptr1 is now empty
cout << "ptr1: " << ptr1.get() << endl;
cout << "ptr2: " << ptr2.get()
Output:
Area = 21
Perimeter = 20
Why is auto_ptr deprecated?
Ownership Transfer:When copying or assigning an auto_ptr, ownership is transferred, and the source pointer becomes null. This behavior made auto_ptr unsuitable for use in STL containers, which require copy semantics.
Lack of Reference Counting:auto_ptr does not support shared ownership, so it cannot be used in scenarios where multiple pointers need to reference the same object.
2. unique_ptr:unique_ptr was introduced in C++11 to replace auto_ptr. It provides exclusive ownership of a dynamically allocated object and ensures that only one unique_ptr can manage a resource at a time. It prevents copying but supports transferring ownership using the std::move() function.
Example:
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
public:
void display() { cout << "MyClass::display()" << endl; }
};
int main() {
unique_ptr<MyClass> ptr1(new MyClass);
ptr1->display();
// Transfer ownership to ptr2
unique_ptr<MyClass> ptr2 = move(ptr1);
ptr2->display();
// ptr1 is now empty
cout << "ptr1: " << ptr1.get() << endl;
cout << "ptr2: " << ptr2.get() << endl;
return 0;
}
Exclusive Ownership: Only one unique_ptr can own a resource at a time.
Move Semantics: Ownership can be transferred using std::move().
Resource Management: When the unique_ptr goes out of scope, the resource is automatically freed.
3. shared_ptr :shared_ptr provides shared ownership of a dynamically allocated object. It uses a reference counting mechanism to keep track of how many pointers are pointing to the object. The object is only destroyed when the reference count reaches zero, meaning all shared_ptrs referencing the object have been deleted.
In C++, the this pointer refers to the current object of a class and is implicitly passed to all non-static member function calls. It is a hidden argument that provides access to the calling object inside the member function.
Type of this Pointer
The type of the this pointer depends on whether the member function is const, volatile, or both. The this pointer type will either be const ExampleClass* or ExampleClass* based on whether the member function is const or not.
1) Const ExampleClass: When a member function is declared as const, the type of the this pointer inside that function becomes const ExampleClass* const. This ensures that the function cannot modify the object that called it.
Example:
#include <iostream>
using namespace std;
class Demo {
public:
void show() const {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'const Demo* const'
cout << "Const member function called" << endl;
}
};
int main() {
Demo obj;
obj.show();
return 0;
}
2) Non-Const ExampleClass: If the member function is not const, the this pointer is of type ExampleClass* const. This means that the function can modify the state of the object it is called on.
Example:
#include <iostream>
using namespace std;
class Demo {
public:
void show() {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'Demo* const'
cout << "Non-const member function called" << endl;
}
};
int main() {
Demo obj;
obj.show();
return 0;
}
3) Volatile ExampleClass: When a member function is declared as volatile, the type of the this pointer becomes volatile ExampleClass* const. This means that the function can work with objects that are volatile (i.e., objects that can be modified outside the program’s control).
Example:
#include <iostream>
using namespace std;
class Demo {
public:
void show() volatile {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'volatile Demo* const'
cout << "Volatile member function called" << endl;
}
};
int main() {
volatile Demo obj;
obj.show();
return 0;
}
4) Const Volatile ExampleClass: If a member function is declared as both const and volatile, the type of the this pointer becomes const volatile ExampleClass* const.
Example:
#include <iostream>
using namespace std;
class Demo {
public:
void show() const volatile {
// 'this' is implicitly passed as a hidden argument
// The type of 'this' is 'const volatile Demo* const'
cout << "Const volatile member function called" << endl;
}
};
int main() {
const volatile Demo obj;
obj.show();
return 0;
}
“delete this” in C++
Using delete this in C++
The delete operator should ideally not be used on the this pointer, as it can lead to undefined behavior if not handled carefully. However, if it is used, the following considerations must be taken into account:
1) The object must be created using new: The delete operator only works for objects that have been dynamically allocated using the new operator. If an object is created on the stack or as a local variable (i.e., without new), using delete this will result in undefined behavior.
Example:
#include <iostream>
using namespace std;
class MyClass {
public:
void destroy() {
delete this; // Deletes the current object
}
};
int main() {
// Valid: Object created using new
MyClass* obj = new MyClass;
obj->destroy();
obj = nullptr; // Ensure pointer is set to null after deletion
// Invalid: Undefined behavior, object created on the stack
MyClass obj2;
obj2.destroy(); // This will cause undefined behavior
return 0;
}
In the valid case, the object is dynamically allocated, and using delete this will correctly free the memory. In the invalid case, the object is created locally, and deleting a non-dynamic object leads to undefined behavior.
2) Accessing members after delete this leads to undefined behavior : Once delete this is called, the object is destroyed, and any attempt to access its members after deletion results in undefined behavior. The program might appear to work, but accessing members of a deleted object is dangerous and unreliable.
Example:
#include <iostream>
using namespace std;
class MyClass {
int value;
public:
MyClass() : value(42) {}
void destroy() {
delete this;
// Invalid: Undefined behavior
cout << value << endl; // This might work but is unsafe
}
};
int main() {
MyClass* obj = new MyClass;
obj->destroy(); // Calls delete this and tries to access a deleted object
return 0;
}
Output:
42 // This is unpredictable and could vary depending on the system
Passing a Function as a Parameter in C++
In C++, functions can be passed as parameters in various ways. This technique is useful, for instance, when passing custom comparator functions in algorithms like std::sort(). There are three primary ways to pass a function as an argument:
1. Passing a function pointer 2. Using std::function<> 3. Using lambdas
1. Passing a Function Pointer : A function can be passed to another function by passing its address, which can be done through a pointer.
Example:
#include <iostream>
using namespace std;
// Function to add two numbers
int add(int x, int y) { return x + y; }
// Function to multiply two numbers
int multiply(int x, int y) { return x * y; }
// Function that takes a pointer to another function
int execute(int x, int y, int (*func)(int, int)) {
return func(x, y);
}
int main() {
// Pass pointers to the 'add' and 'multiply' functions
cout << "Addition of 15 and 5: " << execute(15, 5, &add) << '\n';
cout << "Multiplication of 15 and 5: " << execute(15, 5, &multiply) << '\n';
return 0;
}
Output:
Addition of 15 and 5: 20
Multiplication of 15 and 5: 75
2. Using std::function<> : From C++11, the std::function<> template class allows passing functions as objects. A std::function<> object can be created using the following format:
#include <functional>
#include <iostream>
using namespace std;
// Define add and multiply functions
int add(int x, int y) { return x + y; }
int multiply(int x, int y) { return x * y; }
// Function that accepts an object of type std::function<>
int execute(int x, int y, function<int(int, int)> func) {
return func(x, y);
}
int main() {
// Pass the function as a parameter using its name
cout << "Addition of 15 and 5: " << execute(15, 5, add) << '\n';
cout << "Multiplication of 15 and 5: " << execute(15, 5, multiply) << '\n';
return 0;
}
Output:
Addition of 15 and 5: 20
Multiplication of 15 and 5: 75
3. Using Lambdas : Lambdas in C++ provide a way to create anonymous function objects in place. This is particularly useful when you need a function for a specific task and don’t want to define it elsewhere.
Example:
#include <functional>
#include <iostream>
using namespace std;
// Function that accepts a lambda as a parameter
int execute(int x, int y, function<int(int, int)> func) {
return func(x, y);
}
int main() {
// Lambda for addition
int result1 = execute(15, 5, [](int x, int y) { return x + y; });
cout << "Addition of 15 and 5: " << result1 << '\n';
// Lambda for multiplication
int result2 = execute(15, 5, [](int x, int y) { return x * y; });
cout << "Multiplication of 15 and 5: " << result2 << '\n';
return 0;
}
Output:
Addition of 15 and 5: 20
Multiplication of 15 and 5: 75
Signals in C++
Signals are interrupts that prompt an operating system (OS) to halt its current task and give attention to the task that triggered the interrupt. These signals can pause or interrupt processes running on the OS. Similarly, C++ provides several signals that can be caught and handled within a program. Below is a list of common signals and their associated operations in C++.
Signal
Operation
SIGINT
Produces a receipt for an active signal
SIGTERM
Sends a termination request to the program
SIGBUS
Indicates a bus error (e.g., accessing an invalid address)
SIGILL
Detects an illegal instruction
SIGALRM
Triggered by the alarm() function when the timer expires
SIGABRT
Signals abnormal termination of a program
SIGSTOP
Cannot be blocked, handled, or ignored; stops a process
SIGSEGV
Indicates invalid access to memory (segmentation fault)
SIGFPE
Signals erroneous arithmetic operations like division by zero
SIGUSR1, SIGUSR2
User-defined signals
signal() Function: The signal() function, provided by the signal library, is used to catch and handle unexpected signals or interrupts in a C++ program.
Syntax:
signal(registered_signal, signal_handler);
The first argument is an integer that represents the signal number.
The second argument is a pointer to the function that will handle the signal.
The signal must be registered with a handler function before it can be caught. The handler function should have a return type of void.
Example:
#include <csignal>
#include <iostream>
using namespace std;
void handle_signal(int signal_num) {
cout << "Received interrupt signal (" << signal_num << ").\n";
exit(signal_num); // Terminate the program
}
int main() {
// Register SIGABRT and set the handler
signal(SIGABRT, handle_signal);
while (true) {
cout << "Running program..." << endl;
}
return 0;
}
When you press Ctrl+C, which generates an interrupt signal (e.g., SIGABRT), the program will terminate and print:
Received interrupt signal (22).
raise() Function : The raise() function is used to generate signals in a program.
Syntax:
raise(signal);
It takes a signal from the predefined list as its argument.
Example:
// C program to demonstrate the use of relational operators
#include <stdio.h>
int main() {
int num1 = 12, num2 = 8;
// greater than
if (num1 > num2)
printf("num1 is greater than num2\n");
else
printf("num1 is less than or equal to num2\n");
// greater than or equal to
if (num1 >= num2)
printf("num1 is greater than or equal to num2\n");
else
printf("num1 is less than num2\n");
// less than
if (num1 < num2)
printf("num1 is less than num2\n");
else
printf("num1 is greater than or equal to num2\n");
// less than or equal to
if (num1 <= num2)
printf("num1 is less than or equal to num2\n");
else
printf("num1 is greater than num2\n");
// equal to
if (num1 == num2)
printf("num1 is equal to num2\n");
else
printf("num1 and num2 are not equal\n");
// not equal to
if (num1 != num2)
printf("num1 is not equal to num2\n");
else
printf("num1 is equal to num2\n");
return 0;
}
Output:
Running program...
Running program...
Running program...
Caught signal (11).
A namespace in C++ provides a context where you can declare identifiers such as variables, methods, and classes. It helps in organizing code and avoiding name collisions. For instance, if your code has a function named xyz() and a library also has a function with the same name, the compiler will not be able to distinguish between the two without namespaces. A namespace helps resolve this issue by adding context to the function, class, or variable name.
A namespace essentially defines a scope, and one of its major benefits is preventing name collisions. A well-known example is the std namespace in the C++ Standard Library, where various classes, methods, and templates are defined. While coding in C++, we commonly use using namespace std; to access these elements without needing to prefix std:: to each function call or variable access.
Defining a Namespace
To declare a namespace, use the namespace keyword followed by the namespace name:
namespace my_namespace {
int variable;
void function();
class MyClass {};
}
No semicolon is needed after the closing brace of a namespace definition. To access elements within the namespace, use the following syntax:
my_namespace::variable;
my_namespace::function();
Using the using Directive
The using directive allows you to avoid specifying the namespace every time you use a variable or function. By writing using namespace my_namespace;, you tell the compiler that all identifiers in the code are from the given namespace.
#include <iostream>
using namespace std;
namespace first_space {
void func() {
cout << "Inside first_space" << endl;
}
}
namespace second_space {
void func() {
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main() {
func(); // This calls the function from first_space
return 0;
}
Output:
Inside first_space
The using directive applies from the point where it’s used until the end of the scope. If another entity with the same name exists in a broader scope, it gets hidden.
Nested Namespaces
Namespaces can also be nested, where one namespace is defined within another. For example:
Entities, such as variables and functions, defined in a namespace are scoped within that namespace. This means you can have entities with the same name in different namespaces without causing errors.
#include <iostream>
using namespace std;
namespace first_space {
void func() {
cout << "Inside first_space" << endl;
}
}
namespace second_space {
void func() {
cout << "Inside second_space" << endl;
}
}
int main() {
first_space::func(); // Calls function from first_space
second_space::func(); // Calls function from second_space
return 0;
}
Output:
Inside first_space
Inside second_space
Namespaces in Practice
Consider the following code that demonstrates the error caused by using two variables with the same name in the same scope:
int main() {
int value;
value = 0;
double value; // Error: redeclaration of 'value'
value = 0.0;
}
Error:
Compiler Error: 'value' has a previous declaration as 'int value'
With namespaces, you can declare two variables with the same name in different namespaces without conflict:
#include <iostream>
using namespace std;
namespace first_space {
int val = 500;
}
int val = 100;
int main() {
int val = 200;
cout << first_space::val << endl;
return 0;
}
Output:
ns::Geek::display()
Alternatively, a class can be declared inside a namespace and defined outside the namespace:
#include <iostream>
using namespace std;
namespace ns {
class Geek;
}
class ns::Geek {
public:
void display() {
cout << "ns::Geek::display()" << endl;
}
};
int main() {
ns::Geek obj;
obj.display();
return 0;
}
Output:
ns::Geek::display()
Namespaces in C++ (Set 2: Extending and Unnamed Namespaces)
Defining a Namespace:
A namespace definition in C++ starts with the namespace keyword followed by the name of the namespace, like so:
namespace my_namespace
{
// Variable declarations
int my_var;
// Method declarations
void my_function();
// Class declarations
class MyClass {};
}
Note that there is no semicolon after the closing brace of the namespace definition. To use variables or functions from a specific namespace, prepend the namespace name with the scope resolution operator (::), like this:
To avoid manually prepending the namespace name each time, you can use the using namespace directive. This tells the compiler to implicitly use the names from the specified namespace:
#include <iostream>
using namespace std;
// Define namespaces
namespace first_space
{
void func()
{
cout << "Inside first_space" << endl;
}
}
namespace second_space
{
void func()
{
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main()
{
func(); // Calls the function from first_space
return 0;
}
The names introduced by the using directive follow normal scoping rules. Once introduced, they are visible from the point of the directive to the end of the scope where it was used.
Nested Namespaces:
You can also define namespaces within other namespaces, referred to as nested namespaces:
To access members of a nested namespace, use the following syntax:
using namespace outer_space::inner_space;
If you only use outer_space, it will also make the inner namespace available in scope:
#include <iostream>
using namespace std;
namespace outer_space
{
void outer_func()
{
cout << "Inside outer_space" << endl;
}
namespace inner_space
{
void inner_func()
{
cout << "Inside inner_space" << endl;
}
}
}
using namespace outer_space::inner_space;
int main()
{
inner_func(); // Calls function from inner_space
return 0;
}
Creating Multiple Namespaces:
It’s possible to create multiple namespaces with different names in the global scope. Here’s an example:
#include <iostream>
using namespace std;
// First namespace
namespace first
{
int func() { return 5; }
}
// Second namespace
namespace second
{
int func() { return 10; }
}
int main()
{
cout << first::func() << endl; // Calls func() from first namespace
cout << second::func() << endl; // Calls func() from second namespace
return 0;
}
Output:
5
10
Extending Namespaces:
It is possible to define a namespace in parts using the same name more than once. Essentially, the second block is an extension of the first:
#include <iostream>
using namespace std;
namespace first
{
int val1 = 500;
}
namespace first
{
int val2 = 501;
}
int main()
{
cout << first::val1 << endl; // Accesses first part of the namespace
cout << first::val2 << endl; // Accesses second part of the namespace
return 0;
}
Output:
500
501
Unnamed Namespaces:
Unnamed namespaces allow you to define identifiers that are unique to the file they are declared in. They can be seen as a replacement for the old static keyword for file-scope variables. In unnamed namespaces, no namespace name is provided, and the compiler generates a unique name for it:
#include <iostream>
using namespace std;
namespace
{
int rel = 300;
}
int main()
{
cout << rel << endl; // Prints 300
return 0;
}
Output:
300
Namespace in C++ | Set 3 (Accessing, creating header, nesting and aliasing)
Different Ways to Access Namespaces in C++
In C++, there are multiple ways to access variables and functions within a namespace. Here’s an explanation with modified examples.
Defining a Namespace
A namespace definition starts with the namespace keyword followed by the name of the namespace, as shown below:
namespace my_namespace
{
// Variable declaration
int my_var;
// Function declaration
void my_function();
// Class declaration
class MyClass {};
}
There is no semicolon after the closing brace. To access variables or functions from the namespace, use the following syntax:
my_namespace::my_var; // Access variable
my_namespace::my_function(); // Access function
The using Directive
To avoid repeatedly writing the namespace name, you can use the using namespace directive. This informs the compiler to assume that all names used after the directive come from the specified namespace:
#include <iostream>
using namespace std;
// First namespace
namespace first_ns
{
void display()
{
cout << "Inside first_ns" << endl;
}
}
// Second namespace
namespace second_ns
{
void display()
{
cout << "Inside second_ns" << endl;
}
}
using namespace first_ns;
int main()
{
display(); // Calls the function from first_ns
return 0;
}
In this case, the function from first_ns will be called because of the using directive.
Nested Namespaces
Namespaces can be nested, meaning you can define one namespace inside another:
You can access members of a nested namespace using the following syntax:
using namespace outer_ns::inner_ns;
Alternatively, you can also use a more hierarchical approach:
#include <iostream>
using namespace std;
// Outer namespace
namespace outer_ns
{
void outer_function()
{
cout << "Inside outer_ns" << endl;
}
// Inner namespace
namespace inner_ns
{
void inner_function()
{
cout << "Inside inner_ns" << endl;
}
}
}
using namespace outer_ns::inner_ns;
int main()
{
inner_function(); // Calls the function from inner_ns
return 0;
}
Accessing Namespace Members
1. Accessing Normally : You can access members of a namespace using the scope resolution operator (::)
#include <iostream>
using namespace std;
namespace sample_ns
{
int number = 100;
}
int main()
{
cout << sample_ns::number << endl; // Accesses variable with scope resolution
return 0;
}
Output:
100
2. Using the using Directive : You can also use the using directive to make variables and functions directly accessible:
#include <iostream>
using namespace std;
namespace sample_ns
{
int number = 100;
}
// Use of 'using' directive
using namespace sample_ns;
int main()
{
cout << number << endl; // Accesses variable without scope resolution
return 0;
}
Example:
100
Using Namespaces Across Files
Namespaces can be defined in one file and accessed from another. This can be done as follows: